Compare commits

...

353 Commits

Author SHA1 Message Date
Vipul Gupta (@vipulgupta2048)
82d0bba96a patch: Add etcherPro documentation
Signed-off-by: Vipul Gupta (@vipulgupta2048) <vipul@balena.io>
2022-11-08 03:47:51 +05:30
Balena CI
5945ab1f50 v1.7.9 2022-04-22 16:47:29 +03:00
bulldozer-balena[bot]
59d67220d4 Merge pull request #3747 from balena-io/next-release
patch: deb afterinstall and readme updates
2022-04-22 13:45:50 +00:00
mcraa
61610ded84 patch: update allowed extensions to include deb afterinstall in build 2022-04-22 15:08:04 +02:00
Peter Makra
c87a132f40 patch: add update notification 2022-04-22 14:44:36 +02:00
Andrew Scheller
350d4de32b patch: fix usb-device-boot link in README
Change-type: patch
2022-03-31 17:46:08 +02:00
Ken Bannister
f5f9025d6d Fix application directory for Debian postinst script
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2022-03-31 17:45:10 +02:00
Balena CI
549d744d04 v1.7.8 2022-03-18 19:22:12 +02:00
bulldozer-balena[bot]
6194460dc2 Merge pull request #3723 from balena-io/v1.7.8-draft
small ui updates
2022-03-18 17:20:11 +00:00
Peter Makra
8370f638b4 patch: complete suse uninstall readme
Change-Type: patch
2022-03-11 00:40:20 +01:00
Peter Makra
ac34c51125 patch: completed suse instructions
Change-Type: patch
2022-03-11 00:33:29 +01:00
Peter Makra
b241470fe1 patch: order rpm instrictions
Change-Type: patch
2022-03-10 17:00:43 +01:00
Peter Makra
179697040c Merge branch 'v1.7.8-draft' of github.com:balena-io/etcher into v1.7.8-draft 2022-03-10 16:37:01 +01:00
Peter Makra
335766ed12 patch: enabled update notification for version 1.7.8
Change-Type: patch
2022-03-10 16:36:24 +01:00
Peter Makra
4c5d052a71 patch: updated title to balenaEtcher
fixes #3592

Change-Type: patch
2022-03-10 16:35:03 +01:00
Peter Makra
86423342a8 patch: cleanup and organize readme
Change-Type: patch
2022-03-10 16:27:38 +01:00
Peter Makra
d8b41552e3 patch: extend cloudsmith attribution in readme
Change-Type: patch
2022-03-10 16:08:43 +01:00
Logicer
11c65fb392 Update macOS Icon to Big Sur Style
Change-type: patch
2022-03-10 15:55:27 +01:00
Balena CI
bed126506f v1.7.7 2022-02-22 11:25:43 +02:00
bulldozer-balena[bot]
f6aeb52b16 Merge pull request #3711 from balena-io/fix-auto-update
patch: Fix auto update
2022-02-22 09:23:16 +00:00
Peter Makra
a5201942b8 patch: clarified update check 2022-02-22 09:50:34 +01:00
Peter Makra
c1f7164273 patch: autoupdate stagingPercentage check, include default
Change-Type: patch
2022-02-21 21:09:49 +01:00
Balena CI
6774bf784c v1.7.6 2022-02-21 18:00:46 +02:00
bulldozer-balena[bot]
56ec8b4eac Merge pull request #3699 from balena-io/update-issue-template
patch: add requirements and help to issue template
2022-02-21 15:59:05 +00:00
Peter Makra
35868509af patch: version number notification 2022-02-21 15:21:58 +01:00
Peter Makra
3ab6749f49 Merge branch 'master' into update-issue-template 2022-02-21 14:33:16 +01:00
Balena CI
7a012a92bc v1.7.5 2022-02-21 15:19:52 +02:00
bulldozer-balena[bot]
aba01825a0 Merge pull request #3700 from flec/fix-basic-auth
patch: fix flashing from URL when using basic auth
2022-02-21 13:17:31 +00:00
Peter Makra
907a3308de updated branch with 1.7.4 2022-02-21 13:53:16 +01:00
Peter Makra
4366bb372f patch: fixed typos in template 2022-02-21 13:49:18 +01:00
Marco Füllemann
a6f6cd4a19 patch: fix flashing from URL when using basic auth 2022-02-21 13:36:40 +01:00
Balena CI
03ee428039 v1.7.4 2022-02-21 13:44:44 +02:00
bulldozer-balena[bot]
8d652d064d Merge pull request #3704 from balena-io/update-electron-12.2.3
patch: updated electron to 12.2.3
2022-02-21 11:42:46 +00:00
Peter Makra
28adc34239 patch: set version update notification 1.7.3 2022-02-17 14:15:46 +01:00
Peter Makra
120e9bf42f Merge branch 'update-electron-12.2.3' of github.com:balena-io/etcher into update-electron-12.2.3 2022-02-17 13:47:02 +01:00
Peter Makra
59f54e194b patch: updated electron to 12.2.3
Change-Type: patch
2022-02-17 13:46:35 +01:00
Peter Makra
c4834e61a7 patch: updated electron to 12.2.3
Change-Type: patch
2022-02-17 13:37:17 +01:00
mcraa
e4d02bc561 patch: add requirements and help to issue template 2022-02-08 08:19:55 +00:00
mcraa
b9e54e39f7 patch: add requirements and help to issue template 2022-02-08 09:19:55 +01:00
Balena CI
f3c32eac65 v1.7.3 2021-12-29 17:09:42 +02:00
bulldozer-balena[bot]
9a303ab344 Merge pull request #3667 from balena-io/fix-message-of.null
patch: fix mesage of null
2021-12-29 15:06:59 +00:00
Peter Makra
9c1b55bebc patch: fix mesage of null
Change-Type: patch
2021-12-29 15:23:21 +01:00
Balena CI
30ae4bbd86 v1.7.2 2021-12-21 20:38:54 +02:00
bulldozer-balena[bot]
c6126a980a Merge pull request #3661 from balena-io/fix-windows-open-from-web
patch: fixed open from browser on windows
2021-12-21 18:36:22 +00:00
Peter Makra
ef90d048ca patch: fixed open from browser on windows
Change-type: patch
Signed-off-by: Peter Makra <peter@balena.io>
2021-12-21 17:47:10 +01:00
Balena CI
b938132038 v1.7.1 2021-11-29 11:05:10 +02:00
bulldozer-balena[bot]
3cb2e78fe7 Merge pull request #3630 from balena-io/improve-webpack-build
patch: Improve webpack build time
2021-11-29 09:02:29 +00:00
Lorenzo Alberto Maria Ambrosi
ea9875ddf0 patch: Revert back to electron-rebuild
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-22 12:24:26 +01:00
Lorenzo Alberto Maria Ambrosi
65dacd2ff2 patch: Disallow TS in JS
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-16 20:08:11 +01:00
Lorenzo Alberto Maria Ambrosi
a190818827 patch: Remove esInterop TS flag
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 14:39:33 +01:00
Lorenzo Alberto Maria Ambrosi
98e33b619b patch: Use @balena/sudo-prompt
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 14:39:07 +01:00
Lorenzo Alberto Maria Ambrosi
685ed715ac patch: Update rpiboot guide link
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 11:47:11 +01:00
Lorenzo Alberto Maria Ambrosi
3cf3c4b398 patch: Improve webpack build time
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 10:45:00 +01:00
Balena CI
1c2ef4b1d4 v1.7.0 2021-11-09 18:09:26 +02:00
bulldozer-balena[bot]
d22fc91585 Merge pull request #3628 from balena-io/device-info-draft
Device info draft
2021-11-09 16:07:20 +00:00
Lorenzo Alberto Maria Ambrosi
0a28af5c35 patch: Add missing @types/react@16.8.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-09 14:10:18 +01:00
Lorenzo Alberto Maria Ambrosi
0c1e5b88ef patch: Use npm ci in Makefile
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-09 13:53:04 +01:00
Lorenzo Alberto Maria Ambrosi
790201be90 patch: Add draft info boxes for system information
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-27 10:41:56 +02:00
Lorenzo Alberto Maria Ambrosi
d8d379f05e patch: Remove electron-rebuild package
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:42:46 +02:00
Lorenzo Alberto Maria Ambrosi
b5e9701048 patch: Make electron a dev. dependency
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:40:37 +02:00
Lorenzo Alberto Maria Ambrosi
292f86d6f5 patch: Remove electron-rebuild package
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:40:15 +02:00
Lorenzo Alberto Maria Ambrosi
76ca9934c8 patch: Use exact modules versions
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 11:24:32 +02:00
Lorenzo Alberto Maria Ambrosi
37b826ee4e patch: Update etcher-sdk from v6.2.5 to v6.3.0
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:27:32 +02:00
JSReds
1e1bd3c508 Fix write step for Http file process
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-10-13 17:22:57 +02:00
Lorenzo Alberto Maria Ambrosi
00e8f11913 patch: Fix linting errors
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:21:48 +02:00
Lorenzo Alberto Maria Ambrosi
a3c24a26a0 minor: Refactor dependencies installation to avoid custom scripts
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:16:11 +02:00
Lorenzo Alberto Maria Ambrosi
4232928ad8 patch: Fix LEDs init error
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-30 13:05:47 +02:00
Balena CI
b165fb78da v1.6.0 2021-09-24 20:12:18 +03:00
bulldozer-balena[bot]
e9f6c5ead9 Merge pull request #3599 from balena-io/led-color-settings
Led color settings
2021-09-24 17:10:00 +00:00
Marco Füllemann
b2d0c1c9dd add support for basic auth when downloading images from URL
When selecting "Flash from URL" the user can optionally provide a username and password for basic authentication. The authentication input fields are collapsed by default. When the authentication input fields are collapsed after entering values the values are cleared to ensure that the user sees all parameter passed to the server.

Change-Type: minor
Changelog-Entry: Add support for basic auth when downloading images from URL.
2021-09-17 11:16:29 +02:00
Lorenzo Alberto Maria Ambrosi
14d91400a4 patch: Update etcher-sdk from v6.2.1 to v6.2.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-17 11:11:36 +02:00
David Gaspar
d0114aece7 Update Makefile to Apple M1 info
Expanding host architecture detection.

Change-type: patch
2021-09-07 16:56:10 +02:00
Lorenzo Alberto Maria Ambrosi
dff2df4aab Add LED settings for potentially different hardware
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-07 15:20:56 +02:00
Balena CI
13159f93ee v1.5.122 2021-09-02 17:48:21 +03:00
bulldozer-balena[bot]
3ece1fd841 Merge pull request #3590 from balena-io/various-fixes
Various fixes
2021-09-02 14:46:26 +00:00
Lorenzo Alberto Maria Ambrosi
f46963b6b3 Restore image file selection LED-drive pathing
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-02 14:16:18 +02:00
Lorenzo Alberto Maria Ambrosi
b97f4e0031 Update scripts submodule
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-31 16:40:48 +02:00
Lorenzo Alberto Maria Ambrosi
e2d233d74b Change LEDs colours
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-26 20:15:49 +02:00
Lorenzo Alberto Maria Ambrosi
a7ca2e527b Restore windows images warning
Change-type: patch
Changelog-entry: Windows images now show the proper warning again
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-26 20:15:48 +02:00
Mohamed Salah
396a053c0a Fix Update and install with DNF instructions
Change-type: patch
2021-08-26 20:15:48 +02:00
JSReds
d1a3f1cb88 Add possibile authorization as a query param
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-08-26 20:15:48 +02:00
Xtraim
9f96558cdd update the windows part
I choose to add this part because, after the clean the usb stick
could stay in a raw state without creating the new partions,
activating and formatting.
Thanks

Change-type: patch
2021-08-20 08:31:49 +02:00
thambu1710
b3bc589d70 Update SUPPORT.md
Change-type: patch
2021-08-20 08:31:45 +02:00
Seth Falco
18d2c28110 replace make webpack with npm run webpack
Change-type: patch
2021-08-20 08:31:38 +02:00
JSReds
b272ef296d Add loader on image select
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-08-20 08:31:27 +02:00
Zane Hitchcox
32ca28a3a9 add pnp-webpack-plugin
Change-type: patch
2021-08-20 08:31:15 +02:00
Lorenzo Alberto Maria Ambrosi
4d5e5a3b0b Remove redundant codespell dependency/tests
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-20 08:28:48 +02:00
Balena CI
8b3f37102d v1.5.121 2021-07-14 19:55:16 +03:00
bulldozer-balena[bot]
4b74253631 Merge pull request #3489 from balena-io/direct-select-drive
patch: Select drive on list interaction rather than modal closing
2021-07-14 16:52:42 +00:00
Vipul Gupta
a81b552b95 patch: Delete Codeowners 2021-07-02 13:59:11 +02:00
Lorenzo Alberto Maria Ambrosi
53f53c0f75 Add source maps for devtools
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-25 17:28:17 +02:00
Lorenzo Alberto Maria Ambrosi
fdaf5c69d6 Clone submodules when initializing modules
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-24 14:52:22 +02:00
Lorenzo Alberto Maria Ambrosi
061afca5d3 patch: Select drive on list interaction rather than modal closing
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-24 14:52:22 +02:00
Balena CI
ccb08a48f1 v1.5.120 2021-05-11 19:56:49 +03:00
bulldozer-balena[bot]
a8f3d45b12 Merge pull request #3514 from balena-io/add-cloudsmith-reference
Update README to reference Cloudsmith
2021-05-11 16:49:15 +00:00
Lorenzo Alberto Maria Ambrosi
7e333caaf9 Update README to reference Cloudsmith
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-11 17:59:17 +02:00
Balena CI
70229e8684 v1.5.119 2021-05-01 11:45:32 +03:00
bulldozer-balena[bot]
261700389b Merge pull request #3500 from balena-io/new-deb-rpm-ppa
Update readme for new PPA provider
2021-05-01 08:43:36 +00:00
Lorenzo Alberto Maria Ambrosi
250aed2eb1 Update readme for new PPA provider
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-04-30 23:30:08 +02:00
Balena CI
ed1f008fe2 v1.5.118 2021-04-29 11:47:01 +03:00
bulldozer-balena[bot]
e9ce270dab Merge pull request #3495 from balena-io/etcher-dev-server
Etcher dev server
2021-04-29 08:45:03 +00:00
Zane Hitchcox
1ee110bc95 patch: development environment
Add webpack dev server and hot module reloading to get live changes and reloads without reloading the whole electron app.

This patch also runs the development environment in development mode, which is much, much faster on builds and rebuilds.
2021-04-26 21:17:32 -04:00
Zane Hitchcox
33dd07c675 patch: watch files for electron 2021-04-20 22:30:05 -04:00
Balena CI
39ccbbeeda v1.5.117 2021-04-06 14:44:31 +03:00
bulldozer-balena[bot]
55d2400ac7 Merge pull request #3432 from balena-io/electron-11
Electron 11
2021-04-06 11:42:19 +00:00
Alexis Svinartchouk
0bdea5c54c Rename mac releases (keep old naming)
Change-type: patch
2021-04-02 15:52:33 +02:00
Alexis Svinartchouk
3be372d49f Disable spectron tests on macOS
Change-type: patch
2021-04-01 15:48:39 +02:00
Alexis Svinartchouk
d0c66b2c48 Update electron to v12.0.2
Change-type: patch
2021-04-01 12:13:34 +02:00
Alexis Svinartchouk
65082c4790 Update etcher-sdk from 6.1.1 to 6.2.1
Update etcher-sdk from 6.1.1 to 6.2.1

Change-type: patch
2021-03-29 14:11:44 +02:00
Alexis Svinartchouk
e87ed9beed Fix getAppPath() returning an asar file on macOS
Change-type: patch
2021-03-23 17:53:54 +01:00
Andrew Scheller
bc5563d9c2 Grammar fix
"flash directly" sounds odd

Change-type: patch
2021-03-23 14:32:30 +01:00
vlad doster
ad83ab5dcc (docs) update README.md
- fix spelling
- emphasize notes
- add link
- fix macOS to account for new homebrew API

Change-type: patch
2021-03-23 12:00:40 +01:00
Andrew Scheller
0dc1cf9701 Update copyright year in electron-builder.yml
Change-type: patch
2021-03-23 11:55:43 +01:00
Andrew Scheller
11489c6538 Update copyright year in .resinci.json
Change-type: patch
2021-03-23 11:55:29 +01:00
Dugan Chen
2619d4bc86 Separate the Yum and DNF instructions.
Change-type: patch
2021-03-23 11:55:04 +01:00
Alexis Svinartchouk
3730efd350 Set msvs_version to 2019 when rebuilding
Change-type: patch
2021-03-22 17:23:43 +01:00
Alexis Svinartchouk
6ece32c546 Use moduleIds: 'natural' in webpack config to keep js files in arm64 and x64 mac builds identical
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
fd9996a3cc Update electron-builder to 22.10.5
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
f06cc89152 Update spectron to v13
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
c1d7ab3fa9 Update dependencies, use aws4-axios@2.2.1 to avoid adding more dependiencies
Also filter out dmg-license dependencies from the shrinkwrap file
aws4-axios@2.3.0 brings in react-native, see aws/aws-sdk-js-v3#1797

Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
b206483c7c Update scripts to build universal mac dmgs on the ci
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
c3eb8c7b56 Fix beforeBuild.js script to also work on mac
Change-type: patch
2021-03-15 19:26:49 +01:00
Alexis Svinartchouk
0849d4f435 Support building universal dmgs (x64 and arm64) for mac
Change-type: patch
2021-03-15 19:26:49 +01:00
Alexis Svinartchouk
1dba3ae19b Update electron-builder to 22.10.4
Change-type: patch
2021-02-16 15:49:18 +01:00
Alexis Svinartchouk
f33f2e3771 Fix titlebar z-index
Change-type: patch
2021-02-16 15:49:18 +01:00
Alexis Svinartchouk
e56aaed973 Explicitly set contextIsolation to false
Change-type: patch
2021-02-16 15:49:12 +01:00
Alexis Svinartchouk
a4659f038e Update electron from 9.4.1 to 11.2.3
Change-type: patch
2021-02-10 17:51:56 +01:00
Alexis Svinartchouk
cd462818da Update etcher-sdk from 6.1.0 to 6.1.1
Update etcher-sdk from 6.1.0 to 6.1.1

Change-type: patch
2021-02-10 17:50:47 +01:00
Balena CI
37769efbed v1.5.116 2021-02-03 17:56:40 +02:00
bulldozer-balena[bot]
0f70c4bbce Merge pull request #3414 from balena-io/116
116
2021-02-03 15:54:29 +00:00
Alexis Svinartchouk
48b5e8b9d9 Only cleanup temporary decompressed files in child-writer
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
1f138f0ecc Add .versionbot/CHANGELOG.yml
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
73f67e99ca Stop using node-tmp, use withTmpFile from etcher-sdk instead
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
9114da2445 Update etcher-sdk from 5.2.2 to 6.1.0
Update etcher-sdk from 5.2.2 to 6.1.0

Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
554bbcc780 Revert "Change some border colors to have higher contrast"
This reverts commit 8c4edaabba.

Change-type: patch
2021-02-01 19:44:37 +01:00
Alexis Svinartchouk
4db2289cfd Update electron to v9.4.1
Change-type: patch
2021-02-01 19:44:37 +01:00
Alexis Svinartchouk
c15b56bc23 Update etcher-sdk from 5.2.1 to 5.2.2
Update etcher-sdk from 5.2.1 to 5.2.2

Change-type: patch
2021-01-19 18:44:19 +01:00
Balena CI
9f52dda6ae v1.5.115 2021-01-18 14:09:10 +02:00
bulldozer-balena[bot]
fadcefb11a Merge pull request #3413 from balena-io/115
Update etcher-sdk from 5.1.12 to 5.2.1
2021-01-18 12:07:11 +00:00
Alexis Svinartchouk
361c32913c Update etcher-sdk from 5.1.12 to 5.2.1
Update etcher-sdk from 5.1.12 to 5.2.1

Change-type: patch
2021-01-18 10:46:59 +01:00
Balena CI
5c2042198e v1.5.114 2021-01-15 14:30:49 +02:00
bulldozer-balena[bot]
99df53098c Merge pull request #3394 from balena-io/114
114
2021-01-15 12:28:31 +00:00
Alexis Svinartchouk
aa563c87bd Remove libappindicator1 debian dependency
Changelog-entry: Remove libappindicator1 debian dependency
Change-type: patch
2021-01-12 15:22:43 +01:00
Alexis Svinartchouk
1188888956 Update etcher-sdk from 5.1.11 to 5.1.12
Update etcher-sdk from 5.1.11 to 5.1.12

Change-type: patch
2021-01-12 15:22:43 +01:00
Alexis Svinartchouk
f9d7991dc8 Update rendition from 18.8.3 to 19.2.0
Update rendition from 18.8.3 to 19.2.0

Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
53954e81fd Update dependencies
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
f82996bfd1 Update @balena/lint to 5.3.0
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
b74069eb41 Update webpack to v5
Changelog-entry: Update webpack to v5
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
e8c7591751 Fix typo in webpack.config.ts comment
Change-type: patch
2021-01-12 15:22:42 +01:00
Aaron Shaw
3521b61a81 docs: fix quote marks
Fix quote mark styling

Change-type: patch
Signed-off-by: Aaron Shaw <aaron@balena.io>
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
93db90c725 Disable screensaver while flashing (on balena-electron-env)
Change-type: patch
2021-01-12 15:22:42 +01:00
Balena CI
1dc56aed14 v1.5.113 2020-12-17 16:23:23 +02:00
bulldozer-balena[bot]
d814202424 Merge pull request #3377 from balena-io/113
113
2020-12-17 14:20:57 +00:00
Alexis Svinartchouk
c54856a616 Only store the first error for each target
Changelog-entry: Show the first error for each drive (not the last)
Change-type: patch
2020-12-16 12:33:17 +01:00
Alexis Svinartchouk
fc45df270a Fix red leds not showing for failed devices
Change-type: patch
2020-12-14 18:59:40 +01:00
Aaron Shaw
3cde2faed0 docs: add documentation links
add documentation and faq links

Change-Type: patch
Closes: https://github.com/balena-io/etcher/issues/3191
Signed-off-by: Aaron Shaw <aaron@balena.io>
2020-12-14 18:59:40 +01:00
Aaron Shaw
b4b8c89aad docs: update macOS version
Update macOS version as latest version of Electron is 10.10 compatible only (Yosemite)

Change-Type: patch
Signed-off-by: Aaron Shaw <aaron@balena.io>
2020-12-14 18:59:40 +01:00
Alexis Svinartchouk
36d05724c0 Improve hover message when the drive is too small
Changelog-entry: Improve hover message when the drive is too small
Change-type: patch
2020-12-14 18:59:40 +01:00
Alexis Svinartchouk
b1e4e681d1 Update electron to v9.4.0
Changelog-entry: Update electron to v9.4.0
Change-type: patch
2020-12-14 18:59:40 +01:00
Giovanni Garufi
3987078c11 Update npm to v6.14.8
Change-type: patch
2020-12-11 17:51:50 +01:00
Alexis Svinartchouk
de0010eb72 Update rgb leds colors
Change-type: patch
2020-12-10 17:18:54 +01:00
Alexis Svinartchouk
1f94f44b18 Remove unmountOnSuccess setting
Changelog-entry: Remove unmountOnSuccess setting
Change-type: patch
2020-12-10 15:36:19 +01:00
Alexis Svinartchouk
fe0b45cae6 Only show auto-updates setting on supported targets
Change-type: patch
2020-12-10 15:35:37 +01:00
Alexis Svinartchouk
c32e485f27 Remove dead code in settings modal
Change-type: patch
2020-12-10 14:05:08 +01:00
Alexis Svinartchouk
409b78fc21 Fix effective flashing speed calculation for compressed images
Changelog-entry: Fix effective flashing speed calculation for compressed images
Change-type: patch
2020-12-08 17:14:49 +01:00
bulldozer-balena[bot]
2f08142f5a Merge pull request #3379 from balena-io/high-contrast-lines
Change some border colors to have higher contrast
2020-12-08 13:54:20 +00:00
Lorenzo Alberto Maria Ambrosi
8c4edaabba Change some border colors to have higher contrast
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-12-08 14:37:24 +01:00
Alexis Svinartchouk
05497ce85c Update etcher-sdk from 5.1.10 to 5.1.11
Update etcher-sdk from 5.1.10 to 5.1.11

Changelog-entry: Update etcher-sdk from 5.1.10 to 5.1.11
Change-type: patch
2020-12-07 19:31:41 +01:00
Alexis Svinartchouk
d3df2fe57e Update sys-class-rgb-led from 2.1.1 to 3.0.0
Update sys-class-rgb-led from 2.1.1 to 3.0.0

Changelog-entry: Update sys-class-rgb-led from 2.1.1 to 3.0.0
Change-type: patch
2020-12-04 14:11:51 +01:00
Balena CI
a0f07082f2 v1.5.112 2020-12-03 17:19:22 +02:00
bulldozer-balena[bot]
b7efa8e1f0 Merge pull request #3362 from balena-io/112
112
2020-12-03 15:17:28 +00:00
Alexis Svinartchouk
3647457bb5 Add rendition and sys-class-rgb-led to repo.yml
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
2e5a39dcd8 Update sys-class-rgb-led from 2.1.0 to 2.1.1
Update sys-class-rgb-led from 2.1.0 to 2.1.1

Changelog-entry: Update sys-class-rgb-led from 2.1.0 to 2.1.1
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
edabacfb3a Fix spectron test to work on Windows in all cases
Change-type: none
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
f46176fd10 Fix layout when the featured project is not showing
Changelog-entry: Fix layout when the featured project is not showing
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
2158e20380 Improve flashing error handling
Changelog-entry: Improve flashing error handling
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
fa593e33d1 Update repo.yml to enable nested changelogs
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
50730bd3df Fix imports in child-writer.ts
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
4e68955981 Target commit instead of branch name for sudo-prompt
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
3c0084d012 Fix modal content height on Windows
Change-type: patch
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
8bd11a01ae Update etcher-sdk from 5.1.5 to 5.1.10
Update etcher-sdk from 5.1.5 to 5.1.10

Changelog-entry: Update etcher-sdk from 5.1.5 to 5.1.10
Change-type: patch
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
da3a22d0f6 Set useContentSize to true so the size is the same on all platforms
Changelog-entry: Set useContentSize to true so the size is the same on all platforms
Change-type: patch
2020-11-24 17:10:17 +01:00
Balena CI
e708212d41 v1.5.111 2020-11-23 19:54:50 +02:00
bulldozer-balena[bot]
a5ceba8435 Merge pull request #3345 from balena-io/111
111
2020-11-23 17:52:38 +00:00
Alexis Svinartchouk
446e8e1253 Update bl
Change-type: patch
2020-11-20 20:10:35 +01:00
Alexis Svinartchouk
c69b2fa053 Warn when the source drive has no partition table
Changelog-entry: Warn when the source drive has no partition table
Change-type: patch
2020-11-20 15:29:03 +01:00
Alexis Svinartchouk
0597c0e908 Update etcher-sdk to 5.1.5
Change-type: patch
2020-11-20 14:49:41 +01:00
Alexis Svinartchouk
af2b6bc8ca Update typescript to 4.1.2
Change-type: patch
2020-11-20 14:45:44 +01:00
Alexis Svinartchouk
a2c7a542df Use a different icon when no source drive is available
Changelog-entry: Use a different icon when no source drive is available
Change-type: patch
2020-11-20 14:45:18 +01:00
Alexis Svinartchouk
e37ae2743f Update etcher-sdk to 5.1.3
Change-type: patch
2020-11-17 11:33:36 +01:00
Alexis Svinartchouk
644d955f08 Prevent opening more than one file selector
Change-type: patch
2020-11-16 16:14:36 +01:00
Alexis Svinartchouk
e7b4f09021 Allow selecting a locked SD card as the source drive
Changelog-entry: Allow selecting a locked SD card as the source drive
Change-type: patch
2020-11-16 14:16:38 +01:00
Alexis Svinartchouk
1e0a6a3129 Removed disableExplicitDriveSelection setting, use autoSelectAllDrives instead
Change-type: patch
2020-11-13 20:23:07 +01:00
Alexis Svinartchouk
ef3b8915d8 Update etcher-sdk to 5.1.2
Change-type: patch
2020-11-13 18:30:26 +01:00
Alexis Svinartchouk
e58cfd89c5 Add successBannerURL setting
Change-type: patch
2020-11-11 13:31:04 +01:00
Alexis Svinartchouk
1c52379ee3 Add drivesOrder setting
Change-type: patch
2020-11-11 13:30:54 +01:00
Alexis Svinartchouk
e2c2b40690 Remove "Validate write on success" setting
Validation is always enabled, press the "skip" button to skip it.

Changelog-entry: Remove "Validate write on success" setting. Validation is always enabled, press the "skip" button to skip it.
Change-type: patch
2020-11-11 13:30:26 +01:00
Alexis Svinartchouk
bddb89e4a1 Update electron to v9.3.3
Changelog-entry: Update electron to v9.3.3
Change-type: patch
2020-11-11 13:30:18 +01:00
Alexis Svinartchouk
560ed91e2e Update etcher-sdk to 5.1.1, use WASM ext2fs module
Changelog-entry: Update etcher-sdk to 5.1.1, use WASM ext2fs module
Change-type: patch
2020-11-11 13:29:44 +01:00
Balena CI
1f8f7ad7f8 v1.5.110 2020-11-05 13:56:29 +02:00
bulldozer-balena[bot]
a2a0f2ef41 Merge pull request #3325 from balena-io/new-success-screen-2
New success screen 2
2020-11-05 11:54:37 +00:00
Lorenzo Alberto Maria Ambrosi
40e5fb2287 Add primary colors to default flow
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-04 14:01:56 +01:00
Lorenzo Alberto Maria Ambrosi
6c49c71b3f Remove console.log in tests
Change-type: patch
Changelog-entry: Remove console.log in tests
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
deb3db0fff Add more typings & refactor code accordingly
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
4872fa3d6e Fix URL not being selected with custom protocol
Change-type: patch
Changelog-entry: Fix URL not being selected with custom protocol
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
640a7409ee Add dash on table when selecting only some rows
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
a7637ad8d4 Fix settings spacing
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
31409c61ca Use drive-selector's table for flash errors table
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:46 +01:00
Lorenzo Alberto Maria Ambrosi
e74dc9eb60 Update rendition to v18.8.3
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:01:11 +01:00
Lorenzo Alberto Maria Ambrosi
06997fdf29 Fix zoomFactor in webviews
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:01:11 +01:00
Lorenzo Alberto Maria Ambrosi
611e659626 Add retry button to the errors modal in success screen
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:01:07 +01:00
Lorenzo Alberto Maria Ambrosi
e484ae9837 Cleanup after child-process is terminated
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-22 14:08:44 +02:00
Lorenzo Alberto Maria Ambrosi
7e7ca9524e Add skip function to validation
Change-type: patch
Changelog-entry: Add skip function to validation
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-22 14:08:44 +02:00
Lorenzo Alberto Maria Ambrosi
db09b7440d Rework success screen
Change-type: patch
Changelog-entry: Rework success screen
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-22 14:07:20 +02:00
Balena CI
e9603505d2 v1.5.109 2020-09-14 19:27:56 +03:00
bulldozer-balena[bot]
0f45f6aca1 Merge pull request #3297 from balena-io/use-sudo-prompt-fork
Workaround elevation bug on Windows when the username contains an ampersand
2020-09-14 16:25:48 +00:00
Alexis Svinartchouk
0a28a7794d Update ext2fs to v2.0.5
Change-type: patch
2020-09-14 16:08:44 +02:00
Alexis Svinartchouk
7c2644ec51 Workaround elevation bug on Windows when the username contains an ampersand
Changelog-entry: Workaround elevation bug on Windows when the username contains an ampersand
Change-type: patch
2020-09-11 14:40:19 +02:00
Balena CI
ae62812c61 v1.5.108 2020-09-10 20:33:45 +03:00
bulldozer-balena[bot]
68e24df52b Merge pull request #3295 from balena-io/fix-launch-when-path-has-special-characters
Fix content not loading when the app path contains special characters
2020-09-10 17:31:35 +00:00
Alexis Svinartchouk
b9076d01af Fix content not loading when the app path contains special characters
Changelog-entry: Fix content not loading when the app path contains special characters
Change-type: patch
2020-09-09 17:06:04 +02:00
Balena CI
78a5339e3e v1.5.107 2020-09-07 12:50:26 +03:00
bulldozer-balena[bot]
b099770cb1 Merge pull request #3273 from balena-io/add-clone-drive
Add clone drive
2020-09-07 09:48:16 +00:00
Lorenzo Alberto Maria Ambrosi
b76366a514 Add more typings & refactor code accordingly
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-04 11:24:10 +02:00
Lorenzo Alberto Maria Ambrosi
eeab351636 Fix tests hanging on array.flatMap
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 19:00:07 +02:00
Alexis Svinartchouk
3e45691d0b Re-enable ext partitions trimming on 32 bit Windows
Changelog-entry: Re-enable ext partitions trimming on 32 bit Windows
Change-type: patch
2020-09-02 17:42:52 +02:00
Lorenzo Alberto Maria Ambrosi
f9d79521a1 Fix tests not running
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 17:41:33 +02:00
Lorenzo Alberto Maria Ambrosi
14a89b3b8a Remove lodash from selection-state.ts
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 17:41:33 +02:00
Lorenzo Alberto Maria Ambrosi
8fa6e618c4 Use pretty-bytes instead of custom function
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 17:41:31 +02:00
Lorenzo Alberto Maria Ambrosi
093008dee7 Rework system & large drives handling logic
Change-type: patch
Changelog-entry: Rework system & large drives handling logic
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 17:41:09 +02:00
Lorenzo Alberto Maria Ambrosi
42838eba09 Override cached window's zoomFactor
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:42 +02:00
Lorenzo Alberto Maria Ambrosi
aa72c5d3bb Ignore vscode workspace folder
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:42 +02:00
Lorenzo Alberto Maria Ambrosi
bb04098062 Reword macOS Catalina askpass message
Change-type: patch
Changelog-entry: Reword macOS Catalina askpass message
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:41 +02:00
Lorenzo Alberto Maria Ambrosi
dda022df37 Add clone-drive workflow
Change-type: patch
Changelog-entry: Add clone-drive workflow
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:41 +02:00
Lorenzo Alberto Maria Ambrosi
377dfb8e22 Split drive selector from target selector
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:41 +02:00
Balena CI
07befd0bd1 v1.5.106 2020-08-27 19:18:47 +03:00
bulldozer-balena[bot]
2635a410df Merge pull request #3286 from balena-io/106
106
2020-08-27 16:16:30 +00:00
Alexis Svinartchouk
5e5f82c4b5 Update etcher-sdk to 4.1.29
Changelog-entry: Disable ext partitions trimming on 32 bit windows until it is fixed
Change-type: patch
2020-08-27 15:21:03 +02:00
Alexis Svinartchouk
991cbf6b7f Update etcher-sdk to 4.1.28
Change-type: patch
2020-08-27 12:35:52 +02:00
Alexis Svinartchouk
688d697a99 Update typescript to ^4
Change-type: patch
2020-08-27 12:35:48 +02:00
Alexis Svinartchouk
7894a67719 Fix opening zip files from servers accepting Range headers
Changelog-entry: Fix opening zip files from servers accepting Range headers
Change-type: patch
2020-08-26 18:58:12 +02:00
Balena CI
7a7ea74984 v1.5.105 2020-08-26 14:13:18 +03:00
bulldozer-balena[bot]
12cd8a39c1 Merge pull request #3284 from balena-io/105
105
2020-08-26 11:11:16 +00:00
Alexis Svinartchouk
2c07538f8f Simplify MainPage
Change-type: patch
2020-08-26 00:36:38 +02:00
Alexis Svinartchouk
c9bfd350ed Remove unused FlashStep.props.isWebviewShowing
Change-type: patch
2020-08-26 00:36:38 +02:00
Alexis Svinartchouk
a485d2b4df Remove FeaturedProject class, replace with SafeWebview
Change-type: patch
2020-08-26 00:36:38 +02:00
Alexis Svinartchouk
8ed5ff25a5 Remove unused FeaturedProject.state.show
Change-type: patch
2020-08-26 00:36:38 +02:00
Alexis Svinartchouk
a17a919c37 Remove unused SafeWebvuew.refreshNow property
Change-type: patch
2020-08-26 00:36:33 +02:00
Alexis Svinartchouk
55cafb9268 Update etcher-sdk to 4.1.26
Changelog-entry: Update etcher-sdk to 4.1.26
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
92dfdc6edd URL selector cancel button cancels ongoing url selection
Changelog-entry: URL selector cancel button cancels ongoing url selection
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
fff9452509 Spinner for URL selector modal
Changelog-entry: Spinner for URL selector modal
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
27e560c961 Update rendition to ^18.4.1
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
34489f0d66 Update etcher-sdk to 4.1.25
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
b7f8c8368c Fix settings button not being clickable
Change-type: patch
2020-08-26 00:36:32 +02:00
Balena CI
f383f0be6c v1.5.104 2020-08-21 16:01:18 +03:00
bulldozer-balena[bot]
ff08cb44f9 Merge pull request #3281 from balena-io/104
Fix saving settings, update electron
2020-08-21 12:59:24 +00:00
Alexis Svinartchouk
6cb914e969 Update etcher-sdk to v4.1.24
Chanelog-entry: Update etcher-sdk to v4.1.24
Change-type: patch
2020-08-20 20:54:20 +02:00
Alexis Svinartchouk
a24be20e95 Fix writing config file
Changelog-entry: Fix writing config file
Change-type: patch
2020-08-20 17:27:24 +02:00
Alexis Svinartchouk
08716efbd5 Update rendition to 18.1.0
Change-type: patch
2020-08-20 16:40:19 +02:00
Alexis Svinartchouk
24c8ede746 Remove unused part of Makefile
Change-type: patch
2020-08-20 12:45:59 +02:00
Alexis Svinartchouk
548475996c Remove duplicated styled-system
Change-type: patch
2020-08-20 12:24:09 +02:00
Alexis Svinartchouk
7f9add3f1e Remove no longer used nan
Change-type: patch
2020-08-20 11:53:13 +02:00
Alexis Svinartchouk
6eab47259e Remove no longer used @types/request
Change-type: patch
2020-08-20 11:42:04 +02:00
Alexis Svinartchouk
46663e3a6f Remove no longer used @types/bluebird
Change-type: patch
2020-08-20 11:40:37 +02:00
Alexis Svinartchouk
9797a2152d Update electron to v9.2.1
Changelog-entry: Update electron to v9.2.1
Change-type: patch
2020-08-20 11:37:14 +02:00
Alexis Svinartchouk
a7c3431556 Remove unused error message
Change-type: patch
2020-08-20 11:35:55 +02:00
Balena CI
fef9cd7bec v1.5.103 2020-08-19 14:57:18 +03:00
bulldozer-balena[bot]
b2c4f7a250 Merge pull request #3270 from balena-io/remove-bluebird
Remove bluebird
2020-08-19 11:55:07 +00:00
Alexis Svinartchouk
88ae9fcbd1 Update dependencies
Change-type: patch
2020-08-18 20:02:07 +02:00
Alexis Svinartchouk
bc092114c1 Don't use more than a 8th of the system memory as buffers
Change-type: patch
2020-08-18 17:14:23 +02:00
Alexis Svinartchouk
9f29dc8b76 Update rendition to ^17
Changelog-entry: Update rendition  to ^17
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
5fbaa3a3db Update @balena/udif, don't bundle htmlparser2 into the writer
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
0c59168ceb Change isFocused check to isVisible in tests
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
540fe90609 Fix running tests on Windows
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
1f44f3944f Update electron to 9.2.0
Changelog-entry: Update electron to 9.2.0
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
fbacb8187d Update etcher-sdk to ^4.1.23
Changelog-entry: Update etcher-sdk to ^4.1.23
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
ac2d4ae8f3 Move linting and testing into package.json
Changelog-entry: Move linting and testing into package.json
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
a3322e9fd7 Set module: es2015 in tsconfig.json
Changelog-entry: Set module: es2015 in tsconfig.json
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
281f119456 Replace native elevator with sudo-prompt on windows
Changelog-entry: Replace native elevator with sudo-prompt on windows
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
140f3452ed Don't import WeakMap polyfill in deep-map-keys
Changelog-entry: Don't import WeakMap polyfill in deep-map-keys
Change-type: patch
2020-08-06 16:19:34 +02:00
Alexis Svinartchouk
481be42eb5 Update etcher-sdk to ^4.1.22
Change-type: patch
2020-08-06 16:19:32 +02:00
Alexis Svinartchouk
f2a37079eb Don't use lodash in child-writer.js
Changelog-entry: Don't use lodash in child-writer.js
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
76fa698995 Optimize svgs
Changelog-entry: Optimize svgs
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
f8e21e2338 User regular stream in lzma-native instead of readable-stream
Changelog-entry: User regular stream in lzma-native instead of readable-stream
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
482c29bc2a Update dependencies
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
0bf1ec4958 Remove Bluebird
Changelog-entry: Remove Bluebird
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
3b105d5a6a Update etcher-sdk to ^4.1.20
Change-type: patch
2020-08-06 15:40:39 +02:00
Balena CI
6d9c81da43 v1.5.102 2020-07-27 18:57:16 +03:00
bulldozer-balena[bot]
c2e23855b3 Merge pull request #3247 from balena-io/lighter
Lighter
2020-07-27 15:55:14 +00:00
Alexis Svinartchouk
3f59d35fb6 Update etcher-sdk to ^4.1.19
Changelog-entry: Fix flashing truncated images, fix flashing large dmgs
Change-type: patch
2020-07-27 13:11:27 +02:00
Alexis Svinartchouk
44c74f33d9 Electron 9.1.1
Changelog-entry: Electron 9.1.1
Change-type: patch
2020-07-27 13:11:27 +02:00
Alexis Svinartchouk
512785e0a9 Remove bluebird from main process, reduce lodash usage
Changelog-entry: Remove bluebird from main process, reduce lodash usage
Change-type: patch
2020-07-20 11:11:41 +02:00
Alexis Svinartchouk
963fc574c3 Centralize imports in child-writer
Changelog-entry: Centralize imports in child-writer
Change-type: patch
2020-07-16 18:52:37 +02:00
Alexis Svinartchouk
3218fc2c83 Split main process and child-writer js files
Changelog-entry: Split main process and child-writer js files
Change-type: patch
2020-07-16 18:52:28 +02:00
Alexis Svinartchouk
dc9351713c Stop using request, replace it with already used axios
Changelog-entry: Stop using request, replace it with already used axios
Change-type: patch
2020-07-16 18:52:19 +02:00
Alexis Svinartchouk
e72049d6e8 Remove font awesome unused icons from the generated bundle
Changelog-entry: Remove font awesome unused icons from the generated bundle
Change-type: patch
2020-07-16 18:52:11 +02:00
Alexis Svinartchouk
170126a490 Remove no longer used .sass-lint.yml
Changelog-entry: Remove no longer used .sass-lint.yml
Change-type: patch
2020-07-16 18:52:04 +02:00
Alexis Svinartchouk
7d53d0aadc Use tslib
Changelog-entry: Use tslib
Change-type: patch
2020-07-16 18:51:52 +02:00
Alexis Svinartchouk
5eac622b8c Use strict typescript compiler option
Changelog-entry: Use strict typescript compiler option
Change-type: patch
2020-07-16 18:51:42 +02:00
Alexis Svinartchouk
175e41de8d Update rendition to ^16.1.1
Changelog-entry: Update rendition to ^16.1.1
Change-type: patch
2020-07-16 18:51:12 +02:00
Balena CI
61f4762341 v1.5.101 2020-07-09 19:39:12 +03:00
bulldozer-balena[bot]
7c24d1486f Merge pull request #3222 from balena-io/efp-restyle
Efp restyle
2020-07-09 16:37:26 +00:00
Lorenzo Alberto Maria Ambrosi
630f6c691c Resize modal to show content appropriately
Change-type: patch
Changelog-entry: Resize modal to show content appropriately
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-07-09 15:09:28 +02:00
Alexis Svinartchouk
5c5273bd6c autoSelectAllDrives setting
Change-type: patch
2020-07-01 18:58:54 +02:00
Alexis Svinartchouk
9bde38df5a Update etcher-sdk to 4.1.17
Change-type: patch
2020-07-01 15:40:37 +02:00
Alexis Svinartchouk
391e4444d4 Deselect the image if the source drive is removed
Change-type: patch
2020-07-01 12:58:36 +02:00
Alexis Svinartchouk
e5ee0f1961 Mount source drive if automountOnFileSelect is set
Change-type: patch
2020-06-29 14:08:44 +02:00
Alexis Svinartchouk
c8737806c0 Remove unused packages
Change-type: patch
2020-06-29 13:05:31 +02:00
Alexis Svinartchouk
953f572b53 Fix modal not showing overflowing elements
Change-type: patch
2020-06-29 12:57:42 +02:00
Alexis Svinartchouk
05d0f7142d Update rendition to 15.2.4
Change-type: patch
2020-06-29 12:57:25 +02:00
Alexis Svinartchouk
ba29d76a00 Update electron to 9.0.5
Change-type: patch
2020-06-29 12:42:28 +02:00
Alexis Svinartchouk
692274691e Remove non relevant comment
Change-type: patch
2020-06-29 12:38:22 +02:00
Lorenzo Alberto Maria Ambrosi
394d3e0bf2 Update etcher-sdk to v4.1.16
Change-type: patch
Changelog-entry: Update etcher-sdk to v4.1.16
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-25 21:40:02 +02:00
Lorenzo Alberto Maria Ambrosi
784dd03ba7 Convert sass to plain css
Change-type: patch
Changelog-entry: Convert sass to plain css
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-25 18:54:04 +02:00
Lorenzo Alberto Maria Ambrosi
8560189a1e Remove unused scss
Change-type: patch
Changelog-entry: Remove unused scss
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-24 19:05:38 +02:00
Lorenzo Alberto Maria Ambrosi
098ca9a9a1 Remove unused warning in settings
Change-type: patch
Changelog-entry: Remove unused warning in settings
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-23 11:46:09 +02:00
Lorenzo Alberto Maria Ambrosi
3ca50a1e2d Refactor UI without bootstrap & flexboxgrid
Change-type: patch
Changelog-entry: Refactor UI without bootstrap & flexboxgrid
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-23 11:22:33 +02:00
Lorenzo Alberto Maria Ambrosi
00f193541d Restyle modals
Change-type: patch
Changelog-entry: Restyle modals
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-23 09:23:49 +02:00
Lorenzo Alberto Maria Ambrosi
8ce9eac704 Remove bootstrap & flexboxgrid
Change-type: patch
Changelog-entry: Remove bootstrap & flexboxgrid
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-23 09:13:31 +02:00
Lorenzo Alberto Maria Ambrosi
76086a8f91 Rework and move flashing view elements
Change-type: patch
Changelog-entry: Rework and move flashing view elements
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-22 19:19:09 +02:00
Lorenzo Alberto Maria Ambrosi
9b71772e35 Refactor UI grid to use rendition
Change-type: patch
Changelog-entry: Refactor UI grid to use rendition
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-22 19:19:09 +02:00
Balena CI
72e5631167 v1.5.100 2020-06-22 19:57:51 +03:00
bulldozer-balena[bot]
339c7d56bd Merge pull request #3203 from balena-io/new-target-selector
New target selector
2020-06-22 16:08:47 +00:00
Alexis Svinartchouk
ba16995070 Show system drives last
Change-type: patch
2020-06-22 16:53:44 +02:00
Alexis Svinartchouk
b32c4ee728 Update partitioninfo to 5.3.5
Changelog-entry: Update partitioninfo to 5.3.5
Change-type: patch
2020-06-22 15:07:16 +02:00
Lorenzo Alberto Maria Ambrosi
14e4cbf749 Add icon to plug targets in targets modal
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-19 17:15:26 +02:00
Alexis Svinartchouk
406955ca3e Add .vhd to the list of supported extensions, allow opening any file
Changelog-entry: Add .vhd to the list of supported extensions, allow opening any file
Change-type: patch
2020-06-19 16:54:17 +02:00
Alexis Svinartchouk
5a45f8b122 Update target selector ok button label to show the number of selected devices
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
129e7e20e8 Update mocha to v8.0.1
Changelog-entry: Update mocha to v8.0.1
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
7165a8190b Update electron-notarize to v1.0.0
Changelog-entry: Update electron-notarize to v1.0.0
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
07fde0d73f Don't mutate usbboot drives when updating progress
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
a360370c4e Update electron to v9.0.4
Changelog-entry: Update electron to v9.0.4
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
92cd3d688d Update etcher-sdk to v4.1.15
Changelog-entry: Update etcher-sdk to v4.1.15
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
6554ccf0f8 Sticky header in target selection table
Changelog-entry: Sticky header in target selection table
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
9444f0e1b1 Stricter types in target-selector-modal.tsx
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
d63f5eca0d Update rendition to 15.2.1
Changelog-entry: Update rendition to 15.2.1
2020-06-19 16:29:37 +02:00
Lorenzo Alberto Maria Ambrosi
e39fed1f25 Fix source-selector image height
Change-type: patch
Changelog-entry: Fix source-selector image height
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-17 17:38:37 +02:00
Lorenzo Alberto Maria Ambrosi
2dc359b19c Make TargetSelectorModal a React.Component
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-17 17:38:36 +02:00
Lorenzo Alberto Maria Ambrosi
7aec8a4ae2 Refactor styles
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:44:32 +02:00
Lorenzo Alberto Maria Ambrosi
af9d3ba9f1 Update rendition to v15.0.0
Change-type: patch
Changelog-entry: Update rendition to v15.0.0
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:44:32 +02:00
Lorenzo Alberto Maria Ambrosi
b0c71b21b3 Merge unsafe mode with new target selector
Change-type: patch
Changelog-entry: Merge unsafe mode with new target selector
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:44:31 +02:00
Lorenzo Alberto Maria Ambrosi
71c7fbd3a2 Rework target selector modal
Change-type: patch
Changelog-entry: Rework target selector modal
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:44:31 +02:00
Lorenzo Alberto Maria Ambrosi
f8cc7c36b4 Add warning color to Flash! button
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:39:41 +02:00
Balena CI
5d95fcb81f v1.5.99 2020-06-12 15:31:15 +03:00
bulldozer-balena[bot]
d481536a3f Merge pull request #3210 from balena-io/inline-svgs
Inline svgs
2020-06-12 12:29:11 +00:00
Alexis Svinartchouk
62b42e9254 Update node-raspberrypi-usbboot to 0.2.8
Changelog-entry: Update node-raspberrypi-usbboot to 0.2.8
Change-type: patch
2020-06-11 19:26:20 +02:00
Alexis Svinartchouk
03e3354d50 Update electron to 9.0.3
Changelog-entry: Update electron to 9.0.3
Change-type: patch
2020-06-11 19:22:13 +02:00
Alexis Svinartchouk
f01f1ddd7a Inline all svgs
Changelog-entry: Inline all svgs
Change-type: patch
2020-06-11 19:22:13 +02:00
Balena CI
2cb58bbbf0 v1.5.98 2020-06-10 23:36:06 +03:00
bulldozer-balena[bot]
2aedea3139 Merge pull request #3208 from balena-io/update-etcher-sdk-4.1.13
Update etcher sdk 4.1.13
2020-06-10 20:34:02 +00:00
Alexis Svinartchouk
59e37182be Use between 2 and 256MiB for buffering depending on the number of drives
Changelog-entry: Use between 2 and 256MiB for buffering depending on the number of drives
Change-type: patch
2020-06-10 14:52:04 +02:00
Alexis Svinartchouk
52bdd02a4b Check that argument is an url or a regular file before opening
Changelog-entry: Check that argument is an url or a regular file before opening
Change-type: patch
2020-06-10 14:48:44 +02:00
Alexis Svinartchouk
b1376dfa73 Update etcher-sdk to ^4.1.13
Changelog-entry: Update etcher-sdk to ^4.1.13
Change-type: patch
2020-06-10 12:27:37 +02:00
131 changed files with 34392 additions and 20311 deletions

View File

@@ -7,7 +7,6 @@ indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

1
.gitattributes vendored
View File

@@ -27,6 +27,7 @@ Makefile text
*.yml text
*.patch text
*.txt text
*.tpl text
CODEOWNERS text
*.plist text

View File

@@ -1,6 +1,11 @@
- **Etcher version:**
- **Operating system and architecture:**
- **Image flashed:**
- **What do you think should have happened:** <!-- or a step by step reproduction process -->
- **What happened:**
- **Do you see any meaningful error information in the DevTools?**
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->

4
.gitignore vendored
View File

@@ -47,3 +47,7 @@ node_modules
# OSX files
.DS_Store
# VSCode files
.vscode

View File

@@ -15,22 +15,22 @@
},
"builder": {
"appId": "io.balena.etcher",
"copyright": "Copyright 2016-2020 Balena Ltd",
"copyright": "Copyright 2016-2021 Balena Ltd",
"productName": "balenaEtcher",
"nodeGypRebuild": true,
"nodeGypRebuild": false,
"afterPack": "./afterPack.js",
"asar": false,
"files": [
"generated",
"lib/shared/catalina-sudo/sudo-askpass.osascript.js"
],
"beforeBuild": "./beforeBuild.js",
"afterSign": "./afterSignHook.js",
"mac": {
"category": "public.app-category.developer-tools",
"hardenedRuntime": true,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist"
"entitlementsInherit": "entitlements.mac.plist",
"artifactName": "${productName}-${version}.${ext}"
},
"dmg": {
"iconSize": 110,

View File

@@ -1,17 +0,0 @@
# sass-lint config generated by make-sass-lint-config v0.1.2
files:
include: lib/gui/scss/**/*.scss
options:
formatter: stylish
merge-default-rules: false
rules:
no-css-comments: 0
no-important: 0
no-qualifying-elements: 0
placeholder-in-extend: 0
property-sort-order: 0
quotes:
- 1
- style: double

11669
.versionbot/CHANGELOG.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,920 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
# v1.7.9
## (2022-04-22)
* patch: update allowed extensions to include deb afterinstall in build [mcraa]
* patch: add update notification [Peter Makra]
* patch: fix usb-device-boot link in README [Andrew Scheller]
* Fix application directory for Debian postinst script [Ken Bannister]
# v1.7.8
## (2022-03-18)
* patch: complete suse uninstall readme [Peter Makra]
* patch: completed suse instructions [Peter Makra]
* patch: order rpm instrictions [Peter Makra]
* patch: enabled update notification for version 1.7.8 [Peter Makra]
* patch: updated title to balenaEtcher [Peter Makra]
* patch: cleanup and organize readme [Peter Makra]
* patch: extend cloudsmith attribution in readme [Peter Makra]
* Update macOS Icon to Big Sur Style [Logicer]
# v1.7.7
## (2022-02-22)
* patch: clarified update check [Peter Makra]
* patch: autoupdate stagingPercentage check, include default [Peter Makra]
# v1.7.6
## (2022-02-21)
* patch: version number notification [Peter Makra]
* patch: fixed typos in template [Peter Makra]
* patch: add requirements and help to issue template [mcraa]
* patch: add requirements and help to issue template [mcraa]
# v1.7.5
## (2022-02-21)
* patch: fix flashing from URL when using basic auth [Marco Füllemann]
# v1.7.4
## (2022-02-21)
* patch: set version update notification 1.7.3 [Peter Makra]
* patch: updated electron to 12.2.3 [Peter Makra]
* patch: updated electron to 12.2.3 [Peter Makra]
# v1.7.3
## (2021-12-29)
* patch: fix mesage of null [Peter Makra]
# v1.7.2
## (2021-12-21)
* patch: fixed open from browser on windows [Peter Makra]
# v1.7.1
## (2021-11-22)
* patch: Revert back to electron-rebuild [Lorenzo Alberto Maria Ambrosi]
* patch: Disallow TS in JS [Lorenzo Alberto Maria Ambrosi]
* patch: Remove esInterop TS flag [Lorenzo Alberto Maria Ambrosi]
* patch: Use @balena/sudo-prompt [Lorenzo Alberto Maria Ambrosi]
* patch: Update rpiboot guide link [Lorenzo Alberto Maria Ambrosi]
* patch: Improve webpack build time [Lorenzo Alberto Maria Ambrosi]
# v1.7.0
## (2021-11-09)
* patch: Add missing @types/react@16.8.5 [Lorenzo Alberto Maria Ambrosi]
* patch: Use npm ci in Makefile [Lorenzo Alberto Maria Ambrosi]
* patch: Add draft info boxes for system information [Lorenzo Alberto Maria Ambrosi]
* patch: Remove electron-rebuild package [Lorenzo Alberto Maria Ambrosi]
* patch: Make electron a dev. dependency [Lorenzo Alberto Maria Ambrosi]
* patch: Remove electron-rebuild package [Lorenzo Alberto Maria Ambrosi]
* patch: Use exact modules versions [Lorenzo Alberto Maria Ambrosi]
* patch: Update etcher-sdk from v6.2.5 to v6.3.0 [Lorenzo Alberto Maria Ambrosi]
* Fix write step for Http file process [JSReds]
* patch: Fix linting errors [Lorenzo Alberto Maria Ambrosi]
* minor: Refactor dependencies installation to avoid custom scripts [Lorenzo Alberto Maria Ambrosi]
* patch: Fix LEDs init error [Lorenzo Alberto Maria Ambrosi]
# v1.6.0
## (2021-09-20)
* Add support for basic auth when downloading images from URL. [Marco Füllemann]
* patch: Update etcher-sdk from v6.2.1 to v6.2.5 [Lorenzo Alberto Maria Ambrosi]
* Update Makefile to Apple M1 info [David Gaspar]
* Add LED settings for potentially different hardware [Lorenzo Alberto Maria Ambrosi]
# v1.5.122
## (2021-09-02)
* Restore image file selection LED-drive pathing [Lorenzo Alberto Maria Ambrosi]
* Update scripts submodule [Lorenzo Alberto Maria Ambrosi]
* Change LEDs colours [Lorenzo Alberto Maria Ambrosi]
* Windows images now show the proper warning again [Lorenzo Alberto Maria Ambrosi]
* Fix Update and install with DNF instructions [Mohamed Salah]
* Add possibile authorization as a query param [JSReds]
* update the windows part [Xtraim]
* Update SUPPORT.md [thambu1710]
* replace make webpack with npm run webpack [Seth Falco]
* Add loader on image select [JSReds]
* add pnp-webpack-plugin [Zane Hitchcox]
* Remove redundant codespell dependency/tests [Lorenzo Alberto Maria Ambrosi]
# v1.5.121
## (2021-07-05)
* patch: Delete Codeowners [Vipul Gupta]
* Add source maps for devtools [Lorenzo Alberto Maria Ambrosi]
* Clone submodules when initializing modules [Lorenzo Alberto Maria Ambrosi]
* patch: Select drive on list interaction rather than modal closing [Lorenzo Alberto Maria Ambrosi]
# v1.5.120
## (2021-05-11)
* Update README to reference Cloudsmith [Lorenzo Alberto Maria Ambrosi]
# v1.5.119
## (2021-04-30)
* Update readme for new PPA provider [Lorenzo Alberto Maria Ambrosi]
# v1.5.118
## (2021-04-27)
* patch: development environment [Zane Hitchcox]
* patch: watch files for electron [Zane Hitchcox]
# v1.5.117
## (2021-04-02)
* Rename mac releases (keep old naming) [Alexis Svinartchouk]
* Disable spectron tests on macOS [Alexis Svinartchouk]
* Update electron to v12.0.2 [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 6.1.1 to 6.2.1 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-6.2.1
> ### (2021-03-26)
>
>
> <details>
> <summary> Update node-raspberrypi-usbboot from 0.2.11 to 0.3.0 [Alexis Svinartchouk] </summary>
>
>> ### node-raspberrypi-usbboot-0.3.0
>> #### (2021-03-26)
>>
>> * Add support for compute module 4 [Alexis Svinartchouk]
>> * Fix size endianness of boot_message_t message [Alexis Svinartchouk]
>>
> </details>
>
>
> ## etcher-sdk-6.2.0
> ### (2021-02-18)
>
> * Added BeagleBone USB Boot example [Parthiban Gandhi]
> * Added BeagleBone USB Boot support [Parthiban Gandhi]
>
</details>
* Fix getAppPath() returning an asar file on macOS [Alexis Svinartchouk]
* Grammar fix [Andrew Scheller]
* (docs) update README.md [vlad doster]
* Update copyright year in electron-builder.yml [Andrew Scheller]
* Update copyright year in .resinci.json [Andrew Scheller]
* Separate the Yum and DNF instructions. [Dugan Chen]
* Set msvs_version to 2019 when rebuilding [Alexis Svinartchouk]
* Use moduleIds: 'natural' in webpack config to keep js files in arm64 and x64 mac builds identical [Alexis Svinartchouk]
* Update electron-builder to 22.10.5 [Alexis Svinartchouk]
* Update spectron to v13 [Alexis Svinartchouk]
* Update dependencies, use aws4-axios@2.2.1 to avoid adding more dependiencies [Alexis Svinartchouk]
* Update scripts to build universal mac dmgs on the ci [Alexis Svinartchouk]
* Fix beforeBuild.js script to also work on mac [Alexis Svinartchouk]
* Support building universal dmgs (x64 and arm64) for mac [Alexis Svinartchouk]
* Update electron-builder to 22.10.4 [Alexis Svinartchouk]
* Fix titlebar z-index [Alexis Svinartchouk]
* Explicitly set contextIsolation to false [Alexis Svinartchouk]
* Update electron from 9.4.1 to 11.2.3 [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 6.1.0 to 6.1.1 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-6.1.1
> ### (2021-02-10)
>
>
> <details>
> <summary> Update node-raspberrypi-usbboot from 0.2.10 to 0.2.11 [Alexis Svinartchouk] </summary>
>
>> ### node-raspberrypi-usbboot-0.2.11
>> #### (2021-02-10)
>>
>> * Update @balena.io/usb from 1.3.12 to 1.3.14 [Alexis Svinartchouk]
>>
> </details>
>
>
</details>
# v1.5.116
## (2021-02-03)
* Only cleanup temporary decompressed files in child-writer [Alexis Svinartchouk]
* Add .versionbot/CHANGELOG.yml [Alexis Svinartchouk]
* Stop using node-tmp, use withTmpFile from etcher-sdk instead [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 5.2.2 to 6.1.0 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-6.1.0
> ### (2021-02-03)
>
> * Prefix temporary decompressed images filenames [Alexis Svinartchouk]
>
> ## etcher-sdk-6.0.1
> ### (2021-02-02)
>
> * Ignore ENOENT errors on unlink in withTmpFile [Alexis Svinartchouk]
>
> ## etcher-sdk-6.0.0
> ### (2021-02-01)
>
> * Export tmp and add prefix and postfix options [Alexis Svinartchouk]
>
> ## etcher-sdk-5.2.3
> ### (2021-01-26)
>
> * upgrade lint [Zane Hitchcox]
>
</details>
* Revert "Change some border colors to have higher contrast" [Alexis Svinartchouk]
* Update electron to v9.4.1 [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 5.2.1 to 5.2.2 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.2.2
> ### (2021-01-19)
>
>
> <details>
> <summary> Update drivelist from 9.2.2 to 9.2.4 [Alexis Svinartchouk] </summary>
>
>> ### drivelist-9.2.4
>> #### (2021-01-19)
>>
>> * Pass strings between methods as std::string instead of char * [Floris Bos]
>>
>> ### drivelist-9.2.3
>> #### (2021-01-19)
>>
>> * Support lsblk versions that do no support the pttype column [Alexis Svinartchouk]
>>
> </details>
>
>
</details>
# v1.5.115
## (2021-01-18)
<details>
<summary> Update etcher-sdk from 5.1.12 to 5.2.1 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.2.1
> ### (2021-01-15)
>
> * Only run one diskpart at a time [Alexis Svinartchouk]
> * Ignore diskpart VDS_E_DISK_IS_OFFLINE errors [Alexis Svinartchouk]
>
> ## etcher-sdk-5.2.0
> ### (2021-01-06)
>
> * Store progress on usbboot devices [Alexis Svinartchouk]
>
</details>
# v1.5.114
## (2021-01-12)
* Remove libappindicator1 debian dependency [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 5.1.11 to 5.1.12 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.1.12
> ### (2021-01-06)
>
> * Remove BlockDevice.mountpoints incorrect typing [Alexis Svinartchouk]
> * Update axios to 0.21.1 and aws4-axios to 2.0.1 [Alexis Svinartchouk]
>
</details>
<details>
<summary> Update rendition from 18.8.3 to 19.2.0 [Alexis Svinartchouk] </summary>
> ## rendition-19.2.0
> ### (2020-12-29)
>
> * Add truncate property to Txt component [JSReds]
>
> ## rendition-19.1.0
> ### (2020-12-29)
>
> * Add fallback image source to Img component [Stevche Radevski]
>
> ## rendition-19.0.0
> ### (2020-12-21)
>
> * Remove Arcslider component [Stevche Radevski]
>
> ## rendition-18.20.4
> ### (2020-12-17)
>
> * Upgrade rehype-raw to latest version [Kakhaber]
>
> ## rendition-18.20.3
> ### (2020-12-17)
>
> * Fix disabled button tooltip [JSReds]
>
> ## rendition-18.20.2
> ### (2020-12-16)
>
> * Turn keydown handler into an arrow function [Stevche Radevski]
>
> ## rendition-18.20.1
> ### (2020-12-14)
>
> * Fix form not getting the Enter key event when nested in a modal [Stevche Radevski]
>
> ## rendition-18.20.0
> ### (2020-12-14)
>
> * feat: Add new StatsBar component [Graham McCulloch]
>
> ## rendition-18.19.2
> ### (2020-12-14)
>
> * Update snapshots [Graham McCulloch]
> * Removed out-of-date documentation and template text [Graham McCulloch]
>
> ## rendition-18.19.1
> ### (2020-12-04)
>
> * Markdown: Fix line breaks [Kakhaber]
>
> ## rendition-18.19.0
> ### (2020-12-02)
>
> * Make card size responsive [Stevche Radevski]
>
> ## rendition-18.18.0
> ### (2020-12-02)
>
> * Allow passing responsive values to datagrid width props [Stevche Radevski]
>
> ## rendition-18.17.2
> ### (2020-12-01)
>
> * Update snapshots due to a Card change [JSReds]
>
> ## rendition-18.17.1
> ### (2020-12-01)
>
> * Card: make body to be full height [JSReds]
>
> ## rendition-18.17.0
> ### (2020-12-01)
>
> * Add star rating component [Kakhaber]
>
> ## rendition-18.16.0
> ### (2020-11-23)
>
> * Completely revamp the development setup for rendition [Stevche Radevski]
>
> ## rendition-18.15.1
> ### (2020-11-16)
>
> * Modal: Change the button margins to use the predefined spacing palette [Thodoris Greasidis]
>
> ## rendition-18.15.0
> ### (2020-11-16)
>
> * Modal: Move the cancel button first for dangerous & warning actions [Thodoris Greasidis]
>
> ## rendition-18.14.0
> ### (2020-11-16)
>
> * Allow passing checked items as a prop to Table [Stevche Radevski]
>
> ## rendition-18.13.4
> ### (2020-11-16)
>
> * Fix accidental complete lodash import [Thodoris Greasidis]
>
> ## rendition-18.13.3
> ### (2020-11-16)
>
> * Form: Remove the flaky Captcha sceenshot test [Thodoris Greasidis]
> * Update react-simplemde-editor & snapshots for upstream versions [Thodoris Greasidis]
>
> ## rendition-18.13.2
> ### (2020-10-29)
>
> * Updated snapshots [Graham McCulloch]
> * Fix: Confirm only depends on the files it needs [Graham McCulloch]
>
> ## rendition-18.13.1
> ### (2020-10-23)
>
> * Button: Preserve event during confirmation [Kakhaber]
>
> ## rendition-18.13.0
> ### (2020-10-22)
>
> * Button: Add confirmation property [Kakhaber]
>
> ## rendition-18.12.2
> ### (2020-10-21)
>
> * Tabs: changed interfaces and props [JSReds]
>
> ## rendition-18.12.1
> ### (2020-10-20)
>
> * Fix Tabs typings [Stevche Radevski]
>
> ## rendition-18.12.0
> ### (2020-10-19)
>
> * Add a Grid component [Stevche Radevski]
>
> ## rendition-18.11.3
> ### (2020-10-14)
>
> * Added more documentation for JsonSchemaRenderer [Graham McCulloch]
>
> ## rendition-18.11.2
> ### (2020-10-14)
>
> * fix: UI schema for JsonSchemaRenderer DropDownButton and ButtonGroup widgets [Graham McCulloch]
>
> ## rendition-18.11.1
> ### (2020-10-13)
>
> * Add dark mode to storybook [Stevche Radevski]
>
> ## rendition-18.11.0
> ### (2020-10-08)
>
> * Allow passing widget to extraFormats field [Stevche Radevski]
>
> ## rendition-18.10.2
> ### (2020-09-30)
>
> * Resolve module path not relying on node_moules dir [Kakhaber]
>
> ## rendition-18.10.1
> ### (2020-09-29)
>
> * Set tabpanel height so it stretches to full height [StefKors]
> * Specify tabs width to fix layout problems [StefKors]
>
> ## rendition-18.10.0
> ### (2020-09-24)
>
> * feat: Add ColorWidget for JsonSchemaRenderer [Graham McCulloch]
>
> ## rendition-18.9.2
> ### (2020-09-22)
>
> * Markdown: Ignore decorators inside a code block [Kakhaber]
>
> ## rendition-18.9.1
> ### (2020-09-21)
>
> * Add compact variation to tabs [StefKors]
>
> ## rendition-18.9.0
> ### (2020-09-18)
>
> * Improve spacing for Modal and Select components [Stevche Radevski]
>
> ## rendition-18.8.4
> ### (2020-09-17)
>
> * fix: Use widget's display name to reference the widget [Graham McCulloch]
>
</details>
* Update dependencies [Alexis Svinartchouk]
* Update @balena/lint to 5.3.0 [Alexis Svinartchouk]
* Update webpack to v5 [Alexis Svinartchouk]
* Fix typo in webpack.config.ts comment [Alexis Svinartchouk]
* docs: fix quote marks [Aaron Shaw]
* Disable screensaver while flashing (on balena-electron-env) [Alexis Svinartchouk]
# v1.5.113
## (2020-12-16)
* Show the first error for each drive (not the last) [Alexis Svinartchouk]
* Fix red leds not showing for failed devices [Alexis Svinartchouk]
* docs: add documentation links [Aaron Shaw]
* docs: update macOS version [Aaron Shaw]
* Improve hover message when the drive is too small [Alexis Svinartchouk]
* Update electron to v9.4.0 [Alexis Svinartchouk]
* Update npm to v6.14.8 [Giovanni Garufi]
* Update rgb leds colors [Alexis Svinartchouk]
* Remove unmountOnSuccess setting [Alexis Svinartchouk]
* Only show auto-updates setting on supported targets [Alexis Svinartchouk]
* Remove dead code in settings modal [Alexis Svinartchouk]
* Fix effective flashing speed calculation for compressed images [Alexis Svinartchouk]
* Change some border colors to have higher contrast [Lorenzo Alberto Maria Ambrosi]
<details>
<summary> Update etcher-sdk from 5.1.10 to 5.1.11 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.1.11
> ### (2020-12-07)
>
> * Don't use the O_SYNC flag for block devices, only O_DIRECT [Alexis Svinartchouk]
>
</details>
<details>
<summary> Update sys-class-rgb-led from 2.1.1 to 3.0.0 [Alexis Svinartchouk] </summary>
> ## sys-class-rgb-led-3.0.0
> ### (2020-12-03)
>
> * Add example etcher-pro rainbow animation [Alexis Svinartchouk]
> * Use one setInterval instead of a loop for each led, t in seconds [Alexis Svinartchouk]
>
</details>
# v1.5.112
## (2020-12-02)
* Add rendition and sys-class-rgb-led to repo.yml [Alexis Svinartchouk]
<details>
<summary> Update sys-class-rgb-led from 2.1.0 to 2.1.1 [Alexis Svinartchouk] </summary>
> ## sys-class-rgb-led-2.1.1
> ### (2020-12-01)
>
> * Replace resin-lint with @balena/lint [Alexis Svinartchouk]
> * Update typescript to v4.1.2 [Alexis Svinartchouk]
> * Add versionbot changelog [Alexis Svinartchouk]
>
</details>
* Fix layout when the featured project is not showing [Alexis Svinartchouk]
* Improve flashing error handling [Alexis Svinartchouk]
* Fix modal content height on Windows [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 5.1.5 to 5.1.10 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.1.10
> ### (2020-12-02)
>
>
> <details>
> <summary> Update balena-image-fs from 7.0.5 to 7.0.6 [Alexis Svinartchouk] </summary>
>
>> ### balena-image-fs-7.0.6
>> #### (2020-12-02)
>>
>>
>> <details>
>> <summary> Update ext2fs from 3.0.4 to 3.0.5 [Alexis Svinartchouk] </summary>
>>
>>> #### node-ext2fs-3.0.5
>>> ##### (2020-12-02)
>>>
>>> * Fix reading and discarding with offsets > 32 bits [Alexis Svinartchouk]
>>>
>> </details>
>>
>>
> </details>
>
>
> ## etcher-sdk-5.1.9
> ### (2020-12-01)
>
> * Add repo.yml file [Alexis Svinartchouk]
> * Update @balena/udif from 1.1.0 to 1.1.1 [Alexis Svinartchouk]
>
> <details>
> <summary> Update zip-part-stream from 1.0.2 to 1.0.3 [Alexis Svinartchouk] </summary>
>
>> ### zip-part-stream-1.0.3
>> #### (2020-11-30)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update node-raspberrypi-usbboot from 0.2.9 to 0.2.10 [Alexis Svinartchouk] </summary>
>
>> ### node-raspberrypi-usbboot-0.2.10
>> #### (2020-11-30)
>>
>> * Update typescript to v4.1.2 [Alexis Svinartchouk]
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update mountutils from 1.3.19 to 1.3.20 [Alexis Svinartchouk] </summary>
>
>> ### mountutils-1.3.20
>> #### (2020-11-30)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update gzip-stream from 1.1.1 to 1.1.2 [Alexis Svinartchouk] </summary>
>
>> ### gzip-stream-1.1.2
>> #### (2020-11-30)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update drivelist from 9.2.1 to 9.2.2 [Alexis Svinartchouk] </summary>
>
>> ### drivelist-9.2.2
>> #### (2020-11-30)
>>
>> * Update typescript to v4.1.2 [Alexis Svinartchouk]
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update blockmap from 4.0.2 to 4.0.3 [Alexis Svinartchouk] </summary>
>
>> ### blockmap-4.0.3
>> #### (2020-11-30)
>>
>> * Update typescript to v4.1.2 [Alexis Svinartchouk]
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update partitioninfo from 6.0.1 to 6.0.2 [Alexis Svinartchouk] </summary>
>
>> ### partitioninfo-6.0.2
>> #### (2020-11-27)
>>
>>
>> <details>
>> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
>>
>>> #### file-disk-8.0.1
>>> ##### (2020-11-26)
>>>
>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>
>> </details>
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
>
>> ### file-disk-8.0.1
>> #### (2020-11-26)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
>> ### file-disk-8.0.1
>> #### (2020-11-26)
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> <details>
> <summary> Update balena-image-fs from 7.0.4 to 7.0.5 [Alexis Svinartchouk] </summary>
>
>> ### balena-image-fs-7.0.5
>> #### (2020-11-27)
>>
>>
>> <details>
>> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
>>
>>> #### file-disk-8.0.1
>>> ##### (2020-11-26)
>>>
>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>
>> </details>
>>
>>
>> <details>
>> <summary> Update ext2fs from 3.0.3 to 3.0.4 [Alexis Svinartchouk] </summary>
>>
>>> #### node-ext2fs-3.0.4
>>> ##### (2020-11-26)
>>>
>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>
>> </details>
>>
>>
>> <details>
>> <summary> Update partitioninfo from 6.0.1 to 6.0.2 [Alexis Svinartchouk] </summary>
>>
>>> #### partitioninfo-6.0.2
>>> ##### (2020-11-27)
>>>
>>>
>>> <details>
>>> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
>>>
>>>> ##### file-disk-8.0.1
>>>> ###### (2020-11-26)
>>>>
>>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>>
>>> </details>
>>>
>>> * Add versionbot changelog [Alexis Svinartchouk]
>>>
>> </details>
>>
>> * Add versionbot changelog [Alexis Svinartchouk]
>>
> </details>
>
>
> ## etcher-sdk-5.1.8
> ### (2020-11-26)
>
> * Add versionbot changelog [Alexis Svinartchouk]
>
> ## etcher-sdk-5.1.7
> ### (2020-11-25)
>
> * Don't start opening drives in advance to avoid unhandled rejections [Alexis Svinartchouk]
> * Update generated docs [Alexis Svinartchouk]
>
> ## etcher-sdk-5.1.6
> ### (2020-11-24)
>
> * Do not unmount source drives [Alexis Svinartchouk]
> * Factorize retrying transient errors [Alexis Svinartchouk]
> * Retry opening files & block devices on transient errors [Alexis Svinartchouk]
> * Update generated docs [Alexis Svinartchouk]
>
</details>
* Set useContentSize to true so the size is the same on all platforms [Alexis Svinartchouk]
# v1.5.111
## (2020-11-23)
* Warn when the source drive has no partition table [Alexis Svinartchouk]
* Use a different icon when no source drive is available [Alexis Svinartchouk]
* Allow selecting a locked SD card as the source drive [Alexis Svinartchouk]
* Remove "Validate write on success" setting. Validation is always enabled, press the "skip" button to skip it. [Alexis Svinartchouk]
* Update electron to v9.3.3 [Alexis Svinartchouk]
* Update etcher-sdk to 5.1.1, use WASM ext2fs module [Alexis Svinartchouk]
# v1.5.110
## (2020-11-04)
* Remove console.log in tests [Lorenzo Alberto Maria Ambrosi]
* Fix URL not being selected with custom protocol [Lorenzo Alberto Maria Ambrosi]
* Add skip function to validation [Lorenzo Alberto Maria Ambrosi]
* Rework success screen [Lorenzo Alberto Maria Ambrosi]
# v1.5.109
## (2020-09-14)
* Workaround elevation bug on Windows when the username contains an ampersand [Alexis Svinartchouk]
# v1.5.108
## (2020-09-10)
* Fix content not loading when the app path contains special characters [Alexis Svinartchouk]
# v1.5.107
## (2020-09-04)
* Re-enable ext partitions trimming on 32 bit Windows [Alexis Svinartchouk]
* Rework system & large drives handling logic [Lorenzo Alberto Maria Ambrosi]
* Reword macOS Catalina askpass message [Lorenzo Alberto Maria Ambrosi]
* Add clone-drive workflow [Lorenzo Alberto Maria Ambrosi]
# v1.5.106
## (2020-08-27)
* Disable ext partitions trimming on 32 bit windows until it is fixed [Alexis Svinartchouk]
* Fix opening zip files from servers accepting Range headers [Alexis Svinartchouk]
# v1.5.105
## (2020-08-25)
* Update etcher-sdk to 4.1.26 [Alexis Svinartchouk]
* URL selector cancel button cancels ongoing url selection [Alexis Svinartchouk]
* Spinner for URL selector modal [Alexis Svinartchouk]
# v1.5.104
## (2020-08-20)
* Fix writing config file [Alexis Svinartchouk]
* Update electron to v9.2.1 [Alexis Svinartchouk]
# v1.5.103
## (2020-08-18)
* Update rendition to ^17 [Alexis Svinartchouk]
* Update electron to 9.2.0 [Alexis Svinartchouk]
* Update etcher-sdk to ^4.1.23 [Alexis Svinartchouk]
* Move linting and testing into package.json [Alexis Svinartchouk]
* Set module: es2015 in tsconfig.json [Alexis Svinartchouk]
* Replace native elevator with sudo-prompt on windows [Alexis Svinartchouk]
* Don't import WeakMap polyfill in deep-map-keys [Alexis Svinartchouk]
* Don't use lodash in child-writer.js [Alexis Svinartchouk]
* Optimize svgs [Alexis Svinartchouk]
* User regular stream in lzma-native instead of readable-stream [Alexis Svinartchouk]
* Remove Bluebird [Alexis Svinartchouk]
# v1.5.102
## (2020-07-27)
* Fix flashing truncated images, fix flashing large dmgs [Alexis Svinartchouk]
* Electron 9.1.1 [Alexis Svinartchouk]
* Remove bluebird from main process, reduce lodash usage [Alexis Svinartchouk]
* Centralize imports in child-writer [Alexis Svinartchouk]
* Split main process and child-writer js files [Alexis Svinartchouk]
* Stop using request, replace it with already used axios [Alexis Svinartchouk]
* Remove font awesome unused icons from the generated bundle [Alexis Svinartchouk]
* Remove no longer used .sass-lint.yml [Alexis Svinartchouk]
* Use tslib [Alexis Svinartchouk]
* Use strict typescript compiler option [Alexis Svinartchouk]
* Update rendition to ^16.1.1 [Alexis Svinartchouk]
# v1.5.101
## (2020-07-09)
* Resize modal to show content appropriately [Lorenzo Alberto Maria Ambrosi]
* Update etcher-sdk to v4.1.16 [Lorenzo Alberto Maria Ambrosi]
* Convert sass to plain css [Lorenzo Alberto Maria Ambrosi]
* Remove unused scss [Lorenzo Alberto Maria Ambrosi]
* Remove unused warning in settings [Lorenzo Alberto Maria Ambrosi]
* Refactor UI without bootstrap & flexboxgrid [Lorenzo Alberto Maria Ambrosi]
* Restyle modals [Lorenzo Alberto Maria Ambrosi]
* Remove bootstrap & flexboxgrid [Lorenzo Alberto Maria Ambrosi]
* Rework and move flashing view elements [Lorenzo Alberto Maria Ambrosi]
* Refactor UI grid to use rendition [Lorenzo Alberto Maria Ambrosi]
# v1.5.100
## (2020-06-22)
* Update partitioninfo to 5.3.5 [Alexis Svinartchouk]
* Add .vhd to the list of supported extensions, allow opening any file [Alexis Svinartchouk]
* Update mocha to v8.0.1 [Alexis Svinartchouk]
* Update electron-notarize to v1.0.0 [Alexis Svinartchouk]
* Update electron to v9.0.4 [Alexis Svinartchouk]
* Update etcher-sdk to v4.1.15 [Alexis Svinartchouk]
* Sticky header in target selection table [Alexis Svinartchouk]
* Update rendition to 15.2.1 [Alexis Svinartchouk]
* Fix source-selector image height [Lorenzo Alberto Maria Ambrosi]
* Update rendition to v15.0.0 [Lorenzo Alberto Maria Ambrosi]
* Merge unsafe mode with new target selector [Lorenzo Alberto Maria Ambrosi]
* Rework target selector modal [Lorenzo Alberto Maria Ambrosi]
# v1.5.99
## (2020-06-12)
* Update node-raspberrypi-usbboot to 0.2.8 [Alexis Svinartchouk]
* Update electron to 9.0.3 [Alexis Svinartchouk]
* Inline all svgs [Alexis Svinartchouk]
# v1.5.98
## (2020-06-10)
* Use between 2 and 256MiB for buffering depending on the number of drives [Alexis Svinartchouk]
* Check that argument is an url or a regular file before opening [Alexis Svinartchouk]
* Update etcher-sdk to ^4.1.13 [Alexis Svinartchouk]
# v1.5.97
## (2020-06-08)

View File

@@ -1,2 +0,0 @@
* @thundron @zvin @jviotti
/scripts @nazrhom

4
FAQ.md
View File

@@ -37,10 +37,10 @@ modules=xwayland.so
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
## I receive No polkit authentication agent found error in GNU/Linux
## I receive "No polkit authentication agent found" error in GNU/Linux
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
## May I run Etcher in older macOS versions?
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.9 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).

View File

@@ -3,18 +3,12 @@
# ---------------------------------------------------------------------
RESIN_SCRIPTS ?= ./scripts/resin
export NPM_VERSION ?= 6.14.5
export NPM_VERSION ?= 6.14.8
S3_BUCKET = artifacts.ci.balena-cloud.com
# This directory will be completely deleted by the `clean` rule
BUILD_DIRECTORY ?= dist
# See http://stackoverflow.com/a/20763842/1641422
BUILD_DIRECTORY_PARENT = $(dir $(BUILD_DIRECTORY))
ifeq ($(wildcard $(BUILD_DIRECTORY_PARENT).),)
$(error $(BUILD_DIRECTORY_PARENT) does not exist)
endif
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
$(BUILD_DIRECTORY):
@@ -23,9 +17,7 @@ $(BUILD_DIRECTORY):
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
mkdir $@
# See https://stackoverflow.com/a/13468229/1641422
SHELL := /bin/bash
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
# ---------------------------------------------------------------------
# Operating system and architecture detection
@@ -74,6 +66,9 @@ else
ifeq ($(shell uname -m),x86_64)
HOST_ARCH = x64
endif
ifeq ($(shell uname -m),arm64)
HOST_ARCH = aarch64
endif
endif
endif
@@ -93,12 +88,10 @@ TARGET_ARCH ?= $(HOST_ARCH)
# ---------------------------------------------------------------------
# Electron
# ---------------------------------------------------------------------
electron-develop: | $(BUILD_TEMPORARY_DIRECTORY)
$(RESIN_SCRIPTS)/electron/install.sh \
-b $(shell pwd) \
-r $(TARGET_ARCH) \
-s $(PLATFORM) \
-m $(NPM_VERSION)
electron-develop:
git submodule update --init && \
npm ci && \
npm run webpack
electron-test:
$(RESIN_SCRIPTS)/electron/test.sh \
@@ -124,56 +117,20 @@ TARGETS = \
help \
info \
lint \
lint-ts \
lint-sass \
lint-cpp \
lint-spell \
test-spectron \
test-gui \
test \
sanity-checks \
clean \
distclean \
webpack \
electron-develop \
electron-test \
electron-build
webpack:
./node_modules/.bin/webpack
.PHONY: $(TARGETS)
lint-ts:
balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts
lint:
npm run lint
lint-sass:
sass-lint -v lib/gui/app/scss/**/*.scss lib/gui/app/scss/*.scss
lint-cpp:
cpplint --recursive src
lint-spell:
codespell \
--dictionary - \
--dictionary dictionary.txt \
--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 Makefile *.md LICENSE
lint: lint-ts lint-sass lint-cpp lint-spell
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register --require-main "tests/gui/allow-renderer-process-reuse.ts"
test-spectron:
mocha $(MOCHA_OPTIONS) tests/spectron/runner.spec.ts
test-gui:
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.ts
test-sdk:
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.ts
test: test-gui test-sdk test-spectron
test:
npm run test
help:
@echo "Available targets: $(TARGETS)"
@@ -183,15 +140,11 @@ info:
@echo "Host arch : $(HOST_ARCH)"
@echo "Target arch : $(TARGET_ARCH)"
sanity-checks:
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
clean:
rm -rf $(BUILD_DIRECTORY)
distclean: clean
rm -rf node_modules
rm -rf build
rm -rf dist
rm -rf generated
rm -rf $(BUILD_TEMPORARY_DIRECTORY)

175
README.md
View File

@@ -5,16 +5,15 @@
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. It can also flash directly Raspberry Pi devices that support the usbboot protocol
was written correctly, and much more. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode).
[![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)](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)
***
---
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones]
[**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
## Supported Operating Systems
@@ -22,7 +21,7 @@ was written correctly and much more. It can also flash directly Raspberry Pi dev
- macOS 10.10 (Yosemite) and later
- Microsoft Windows 7 and later
Note that Etcher will run on any platform officially supported by
**Note**: Etcher will run on any platform officially supported by
[Electron][electron]. Read more in their
[documentation][electron-supported-platforms].
@@ -31,81 +30,118 @@ Note that Etcher will run on any platform officially supported by
Refer to the [downloads page][etcher] for the latest pre-made
installers for all supported operating systems.
## Packages
> [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge)](https://cloudsmith.com) \
Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com).
Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that
enables your organization to create, store and share packages in any format, to any place, with total
confidence.
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
1. Add Etcher debian repository:
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-deb)
```sh
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
```
1. Add Etcher Debian repository:
2. Trust Bintray.com's GPG key:
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
| sudo -E bash
```
```sh
sudo apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys 379CE192D401AB61
```
2. Update and install:
3. Update and install:
```sh
sudo apt-get update
sudo apt-get install balena-etcher-electron
```
```sh
sudo apt-get update
sudo apt-get install balena-etcher-electron
```
##### Uninstall
```sh
sudo apt-get remove balena-etcher-electron
sudo rm /etc/apt/sources.list.d/balena-etcher.list
sudo apt-get update
rm /etc/apt/sources.list.d/balena-etcher.list
apt-get clean
rm -rf /var/lib/apt/lists/*
apt-get update
```
##### OpenSUSE LEAP & Tumbleweed install
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
##### DNF
1. Add Etcher rpm repository:
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install:
```sh
sudo dnf install -y balena-etcher-electron
```
###### Uninstall
```sh
sudo zypper ar https://balena.io/etcher/static/etcher-rpm.repo
sudo zypper ref
sudo zypper in balena-etcher-electron
rm /etc/yum.repos.d/balena-etcher.repo
rm /etc/yum.repos.d/balena-etcher-source.repo
```
##### Yum
1. Add Etcher rpm repository:
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install:
```sh
sudo yum install -y balena-etcher-electron
```
###### Uninstall
```sh
sudo yum remove -y balena-etcher-electron
rm /etc/yum.repos.d/balena-etcher.repo
rm /etc/yum.repos.d/balena-etcher-source.repo
```
#### OpenSUSE LEAP & Tumbleweed install (zypper)
1. Add the repo
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install
```sh
sudo zypper up
sudo zypper install balena-etcher-electron
```
##### Uninstall
```sh
sudo zypper rm balena-etcher-electron
```
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
1. Add Etcher rpm repository:
```sh
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
```
2. Update and install:
```sh
sudo yum install -y balena-etcher-electron
```
or
```sh
sudo dnf install -y balena-etcher-electron
```
##### Uninstall
```sh
sudo yum remove -y balena-etcher-electron
sudo rm /etc/yum.repos.d/etcher-rpm.repo
sudo yum clean all
sudo yum makecache fast
```
or
```sh
sudo dnf remove -y balena-etcher-electron
sudo rm /etc/yum.repos.d/etcher-rpm.repo
sudo dnf clean all
sudo dnf makecache
# remove the repo
sudo zypper rr balena-etcher
sudo zypper rr balena-etcher-source
```
#### Solus (GNU/Linux x64)
@@ -120,11 +156,10 @@ sudo eopkg it etcher
sudo eopkg rm etcher
```
#### Arch Linux / Manjaro (GNU/Linux x64)
#### Arch/Manjaro Linux (GNU/Linux x64)
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
```sh
yay -S balena-etcher
```
@@ -135,20 +170,20 @@ yay -S balena-etcher
yay -R balena-etcher
```
#### Brew Cask (macOS)
#### Brew (macOS)
Note that the Etcher Cask has to be updated manually to point to new versions,
**Note**: Etcher has to be updated manually to point to new versions,
so it might not refer to the latest version immediately after an Etcher
release.
```sh
brew cask install balenaetcher
brew install balenaetcher
```
##### Uninstall
```sh
brew cask uninstall balenaetcher
brew uninstall balenaetcher
```
#### Chocolatey (Windows)
@@ -168,20 +203,20 @@ choco uninstall etcher
## Support
If you're having any problem, please [raise an issue][newissue] on GitHub and
If you're having any problem, please [raise an issue][newissue] on GitHub, and
the balena.io team will be happy to help.
## License
Etcher is free software, and may be redistributed under the terms specified in
Etcher is free software and may be redistributed under the terms specified in
the [license].
[etcher]: https://balena.io/etcher
[electron]: https://electronjs.org/
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
[SUPPORT]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
[CONTRIBUTING]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
[USER-DOCUMENTATION]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[support]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[milestones]: https://github.com/balena-io/etcher/milestones
[newissue]: https://github.com/balena-io/etcher/issues/new
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE

View File

@@ -1,9 +1,16 @@
Getting help with Etcher
========================
Getting help with BalenaEtcher
===============================
There are various ways to get support for Etcher if you experience an issue or
have an idea you'd like to share with us.
Documentation
------
We have answers to a variety of frequently asked questions in the [user
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
Forums
------
@@ -15,7 +22,7 @@ a look at the existing threads before opening a new one!
Make sure to mention the following information to help us provide better
support:
- The Etcher version you're running.
- The BalenaEtcher version you're running.
- The operating system you're running Etcher in.
@@ -25,10 +32,12 @@ support:
GitHub
------
If you encounter an issue or have a suggestion, head on over to Etcher's [issue
If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue
tracker][issues] and if there isn't a ticket covering it, [create
one][new-issue].
[discourse]: https://forums.balena.io/c/etcher
[issues]: https://github.com/balena-io/etcher/issues
[new-issue]: https://github.com/balena-io/etcher/issues/new
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[faq]: https://etcher.io

11
after-install.tpl Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# Link to the binary
# Must hardcode balenaEtcher directory; no variable available
ln -sf '/opt/balenaEtcher/${executable}' '/usr/bin/${executable}'
# SUID chrome-sandbox for Electron 5+
chmod 4755 '/opt/balenaEtcher/chrome-sandbox' || true
update-mime-database /usr/share/mime || true
update-desktop-database /usr/share/applications || true

Binary file not shown.

View File

@@ -1,26 +0,0 @@
'use strict'
const cp = require('child_process');
const rimraf = require('rimraf');
const process = require('process');
// Rebuild native modules for ia32 and run webpack again for the ia32 part of windows packages
exports.default = function(context) {
if (context.platform.name === 'windows') {
cp.execFileSync(
'bash',
['./node_modules/.bin/electron-rebuild', '--types', 'dev', '--arch', context.arch],
);
rimraf.sync('generated');
cp.execFileSync(
'bash',
['./node_modules/.bin/webpack'],
{
env: {
...process.env,
npm_config_target_arch: context.arch,
},
},
);
}
}

View File

@@ -1,35 +0,0 @@
{
"targets": [
{
"target_name": "elevator",
"include_dirs" : [
"src",
"<!(node -e \"require('nan')\")"
],
'conditions': [
[ 'OS=="win"', {
"sources": [
"src/utils/v8utils.cpp",
"src/os/win32/elevate.cpp",
"src/elevator_init.cpp",
],
"libraries": [
"-lShell32.lib",
],
} ],
[ 'OS=="mac"', {
"xcode_settings": {
"OTHER_CPLUSPLUSFLAGS": [
"-stdlib=libc++"
],
"OTHER_LDFLAGS": [
"-stdlib=libc++"
]
}
} ]
],
}
],
}

View File

@@ -14,9 +14,7 @@ technologies used in Etcher that you should become familiar with:
- [NodeJS][nodejs]
- [Redux][redux]
- [ImmutableJS][immutablejs]
- [Bootstrap][bootstrap]
- [Sass][sass]
- [Flexbox Grid][flexbox-grid]
- [Mocha][mocha]
- [JSDoc][jsdoc]
@@ -67,8 +65,6 @@ be documented instead!
[nodejs]: https://nodejs.org
[redux]: http://redux.js.org
[immutablejs]: http://facebook.github.io/immutable-js/
[bootstrap]: http://getbootstrap.com
[sass]: http://sass-lang.com
[flexbox-grid]: http://flexboxgrid.com
[mocha]: http://mochajs.org
[jsdoc]: http://usejsdoc.org

View File

@@ -91,7 +91,7 @@ make electron-develop
```sh
# Build the GUI
make webpack
npm run webpack
# Start Electron
npm start
```

View File

@@ -159,6 +159,18 @@ pre-installed in all modern Windows versions.
- Run `clean`. This command will completely clean your drive by erasing any
existent filesystem.
- Run `create partition primary`. This command will create a new partition.
- Run `active`. This command will active the partition.
- Run `list partition`. This command will show available partition.
- Run `select partition N`, where `N` corresponds to the id of the newly available partition.
- Run `format override quick`. This command will format the partition. You can choose a specific formatting by adding `FS=xx` where `xx` could be `NTFS or FAT or FAT32` after `format`. Example : `format FS=NTFS override quick`
- Run `exit` to quit diskpart.
### OS X

View File

@@ -0,0 +1,178 @@
---
description: Getting started for etcherPro
slug: /
---
# EtcherPro User manual
## 1. Features
![EtcherPro-manual-01](img/etcher-pro-top-view.jpeg)
![EtcherPro-manual-02](img/etcher-pro-slots.jpeg)
![EtcherPro-manual-03](img/etcher-pro-rear-view.jpeg)
| Specifications | |
| --- | --- |
| Voltage in | 100 to 240 V AC - 10A - 50 to 60 Hz |
| Voltage out | 100 to 240 V AC - 10A - 50 to 60 Hz |
| Ports | 16 x USB 3.0, 16 X SD, 16 x mSD |
| Flashing capacity | 16 x targets when using an online image or, 15 x target drives and 1 x local source drive |
| Daisy-chaining | Up to 10 EtcherPro devices supported (160 x targets) |
| Language support | English |
| Software | balenaEtcher on balenaOS with auto-updates |
| Display | 7in RGB touch screen |
| Network connectivity | WiFi 2.4GHz, 5GHz |
| Working temperature | 5°C ~ 30°C |
| Certifications | CE, FCC |
## 2. Getting started
### 2.1 Setup
### Powering up the device
- EtcherPro is supplied with a mains power cable according to your region's plug standards
- On the back of the device, there are two groups of sockets, labelled as **IN** and **OUT**
- Plug the AC power cable to the socket labelled **POWER IN**, and then to the mains outlet
- The device should boot up automatically; wait until you see the Etcher interface show up
- [Warning] Please avoid using adaptors or extension leads, as this may damage the device or cause it to malfunction
### Connecting to WiFi
- The device will prompt you to connect to a local network the first time it boots (you can skip this step if you are not connecting to WiFi)
- Select the network to which you want to connect, type the password and select **OK**
- You can access the WiFi settings by selecting the WiFi icon at the top left corner of the screen
### 2.2 Etcher functions
### Flash from file
Using an image file as source to flash to one or multiple targets
- From the Etcher menu, select **Flash from file**
- Plug a drive that contains the image you would like to flash into the slot with the blue-colored blinking LED
- Select the image you want to flash from the file browser and select **OK**
- Plug at least one drive or device into an available slot. The plugged target(s) will be selected automatically and the LEDs will turn white
- Select **Flash**, to begin the flashing process
- The LED of each slot will first blink purple for flashing and then green for validating
- Once the flashing is complete, the LEDs will turn green for successfully flashed drives, and red for the failed ones
- You may safely unplug the drives when flashing is complete
### Flash from URL
Using an online image file as source to flash to one or multiple targets
- From the Etcher menu, select **Flash from URL**
- Enter the image URL of you would like to flash in the input field, and select **OK**
- Plug at least one drive or device into an available slot. The plugged target(s) will be selected automatically, and the LEDs will turn white
- Select **Flash**, to begin the flashing process
- The LED of each slot will first blink purple for flashing and then green for validating
- Once the flashing is complete, the LEDs will turn green for successfully flashed drives, and red for the failed ones
- You may safely unplug the drives when flashing is complete
### Clone drive
Using a drive as source, and cloning it to multiple drives
- From the Etcher menu, select **Clone drive**
- Plug a drive you would like to clone into the slot with the blue-colored blinking LED
- The drive will be selected automatically and the LED will stop blinking
- Plug at least one drive into an available slot. The plugged targets will be selected automatically, and the LEDs will turn white
- Select **Flash**, to begin the flashing process
- The LED of each slot will first blink purple for flashing and then green for validating
- Once the flashing is complete, the LEDs will turn green for successfully flashed drives, and red for the failed ones
- You may safely unplug the drives when flashing is complete
### Backup drive
Backing up one or multiple drives into another drive
- From the Etcher menu, select **more options**, then **Backup drive**
- Plug one or more drives you would like to backup into the slot(s) with the blue-colored blinking LED
- The drives will be autoselected and the LED will stop blinking
- Select **OK** to move on to the next step
- Plug the backup drive into the slot with the white-colored blinking LED. The plugged target will be automatically selected and the LED will stop blinking
- Select **Flash**, to begin the flashing process
- The LED of the target slot will first blink purple for flashing, and then green for validating
- Once the flashing is complete, the LED will turn green for a successful flash, and red for a failed one
- You may safely unplug the drive when flashing is complete
### Format drive
Formatting one or multiple drives
- From the Etcher menu, select **more options**, then **Format drive**
- Plug one or more drives you would like to format on the slots with the white-colored blinking LEDs
- The drives will be selected automatically, and the LED will stop blinking
- Select **Flash**, to begin the flashing process
- The LED of the target slot will first blink purple for flashing, and then green for validating
- Once the formatting is complete, the LEDs will turn green for successfully formatted drives, and red for the failed ones
- You may safely unplug the drive when formatting is complete
### 2.3 Daisy-chaining (power only)
Connecting up to 10 EtcherPro devices and power them from one socket.
- EtcherPro allows you to chain power (data chaining will be released later). To do this, you will need a male to female IEC C13/C14 power extension lead (min 10A rated) to connect one EtcherPro to another
- Plug the power extension lead to the 'POWER IN' side of the leading EtcherPro, and then to the 'POWER OUT' side of the successive EtcherPro
- The last EtcherPro of the stack should be plugged directly to the mains power socket
### 2.4 Sleep, wake and power off
- EtcherPro is set to automatically go into sleep mode after a few minutes
- You may change this setting by selecting the settings icon on the top right corner of the screen
- You may put the device to sleep manually by selecting the sleep button on the top left corner of the screen
- To wake up your device, just tap anywhere on the screen
- It is not necessary to power off your device, but if you would like to, you may simply unplug the power-in cable
### 3. Safety and handling
WARNING: Make sure you read and follow the safety and handling instructions before using EtcherPro in order to avoid the potential risk of causing damage to the device, electrical shock, fire, or damage to any other property. If EtcherPro gets physically damaged in any way, or you suspect liquid has leaked into the enclosure, unplug the power cable from the socket and avoid using the device before contacting support (pro@etcher.io).
Handling
It is important not to block the air vents on the back and bottom of the device. As such, we suggest setting up EtcherPro on a well supported desk, with plenty of surrounding space to ensure the device is properly ventilated while in use.
Liquid exposure
EtcherPros enclosure is not waterproof. It is important to keep liquids away from the device to avoid spillages. High humidity environments, rain or snow may also cause damage to the device.
Power
EtcherPro does not have an on/off switch. If you would like to power on the device, you need to plug the power cable into the mains socket. If you would like to power off the device, you need to unplug the power cable. Be sure to unplug the power cable from the socket if you suspect either the cable or the device is physically damaged in some way.
For your own protection and protection of the device, EtcherPro comes with a grounded AC power cable which only fits a grounded mains socket. If you dont have a grounded socket installed, you should contact a specialist who can safely install an appropriate grounded socket. Do not attempt to power on the device without a connected grounding wire or with a power cable that does not meet the original specifications.
Repairing
EtcherPro is not meant to be serviced or repaired by the user. If your device has any issues you should contact support (pro@etcher.io). Attempting to disassemble the device will void the warranty, and could also cause injury or harm.
Radio interference
EtcherPro contains components and radios that emit electromagnetic fields. These electromagnetic fields may interfere with medical devices, such as pacemakers and defibrillators. Consult your physician and medical device manufacturer for information specific to your medical device and whether you need to maintain a safe distance of separation between your medical device(s) and EtcherPro. If you suspect EtcherPro is interfering with your medical device, stop using EtcherPro immediately.
EtcherPro emits electromagnetic fields due to the usage of components and radios. These fields can interfere with other devices and potentially cause them to malfunction. If you suspect EtcherPro is interfering with another device, unplug EtcherPro from the power and contact support (pro@etcher.io).
This equipment is not suitable for use in locations where children are likely to be present.
Atmospheric conditions (dust and vapor)
The EtcherPro enclosure is not sealed. Using the device in an environment that has increased amounts of dust, powder, vapors, corrosive substances, or other contaminants can cause malfunction, injury, and/or fire.
### 4. Warranty
**Limited Product Warranty**
Balena warrants that, for a period of one (1) year after the date of shipment, the Products will be free from defects in materials and workmanship under normal use. As Balenas sole liability and Customers sole and exclusive remedy for any breach of the limited warranty set forth herein, Balena will, at its option and expense, repair or replace any Product returned to Customer during the warranty period that does not comply with such warranty, as confirmed by Balena. Replacement Products will be warranted for the remainder of the original warranty period or ninety (90) days, whichever is longer. All Products that are replaced become the property of Balena. Balena will have no obligation to the extent that any failure of a Product to comply with the limited warranty set forth in this limited product warranty results from or is otherwise attributable to: (i) negligence, misuse, or abuse of the Product; (ii) use of the Product other than in accordance with Balenas published specifications or user manual; (iii) modifications, alterations or repairs to the Product made by a party other than Balena or a party authorized by Balena; (iv) any failure by Customer or a third party to comply with environmental and storage requirements for the Product specified by Balena, including, but not limited to, temperature or humidity ranges; or (v) use of the Product in combination with any third-party devices or products that have not been provided or recommended by Balena. The parties agree that Balenas RMA Policy shall apply to Products returned pursuant to this limited product warranty for a breach of warranty.
THE LIMITED WARRANTY SET FORTH HEREIN IS IN LIEU OF, AND BALENA SPECIFICALLY DISCLAIMS, ANY AND ALL OTHER WARRANTIES AND CONDITIONS, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES OR CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR USAGE OF TRADE. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM BALENA OR ELSEWHERE, WILL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THESE TERMS.
The limited warranty does not apply to:
- Returned items that failed due to an accident, purchasers abuse, neglect or failure to operate in accordance with instructions provided in this refund policy.
- Returned items that failed due to incorrect voltage or improper wiring.
- Returned items that failed due to rain, excessive humidity, corrosive environments, or other contaminants.
- Any item damaged in shipment.
- Any product failure caused by installing or operating product under conditions not in accordance with installation and operation guidelines, or damaged by contact with tools or surroundings.
- Returned items with cosmetic defects that do not interfere with product functionality.
- Returned items that are incomplete or defaced.
- Returned items with a different serial number from what was authorized for return.
- Freight damaged items. If your shipment arrives damaged, you must note the damage on the carrier's delivery record in accordance with the carrier's policy, save the merchandise in the original box and packing it arrived in, and arrange for a carrier inspection of damaged merchandise.
**Initiating a warranty claim**
To initiate a warranty claim, please contact support (pro@etcher.io) to receive a copy of the RMA form. When filling the form, make sure you describe the issue as accurately as possible since it will be used as a basis for determining if the warranty claim is valid or not.
After Balenas evaluation of the return item, Warranty or Out-of-Warranty status will be determined. If the description of the problem is the same as listed on Page 1 of the RMA form, the product will be repaired or replaced under warranty at no charge and shipped back, prepaid, to the customer.
If the description of the problem is different from the problem listed on Page 1 of the RMA form we will contact the customer. At such time, the customer must issue a written confirmation to proceed with the repair(s), agree to cover the costs of the repair and return freight, or authorize the product to be shipped back as is, at the customers expense. Failure to obtain written confirmation within thirty (30) days of notification will result in the product being returned as is, at the customers expense.
If the product has no identifiable problem, we reserve the right to charge for testing and return shipping costs.
For any product returned to balena for reasons other than warranty, a 20% restocking fee and round-trip shipping costs will be deducted from the credit refund. All returned items must be in their original box or crating and must include all packing material, manuals, and accessories.

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
docs/static/img/etcher-pro-slots.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

BIN
docs/static/img/etcher-pro-top-view.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

BIN
docs/static/img/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/static/img/logo.png vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1,10 +1,9 @@
appId: io.balena.etcher
copyright: Copyright 2016-2020 Balena Ltd
copyright: Copyright 2016-2021 Balena Ltd
productName: balenaEtcher
npmRebuild: true
nodeGypRebuild: true
nodeGypRebuild: false
publish: null
beforeBuild: "./beforeBuild.js"
afterPack: "./afterPack.js"
asar: false
files:
@@ -16,6 +15,7 @@ mac:
hardenedRuntime: true
entitlements: "entitlements.mac.plist"
entitlementsInherit: "entitlements.mac.plist"
artifactName: "${productName}-${version}.${ext}"
dmg:
background: assets/dmg/background.tiff
icon: assets/icon.icns
@@ -54,7 +54,6 @@ deb:
depends:
- gconf2
- gconf-service
- libappindicator1
- libasound2
- libatk1.0-0
- libc6
@@ -88,6 +87,7 @@ deb:
- libxss1
- libxtst6
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
afterInstall: "./after-install.tpl"
rpm:
depends:
- util-linux

View File

@@ -23,11 +23,12 @@ import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json';
import { DrivelistDrive, isSourceDrive } from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state';
import { init as ledsInit } from './models/leds';
import { deselectImage, getImage } from './models/selection-state';
import * as settings from './models/settings';
import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics';
@@ -36,15 +37,14 @@ import * as exceptionReporter from './modules/exception-reporter';
import * as osDialog from './os/dialog';
import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage';
import './css/main.css';
window.addEventListener(
'unhandledrejection',
(event: PromiseRejectionEvent | any) => {
// Promise: event.reason
// Bluebird: event.detail.reason
// Anything else: event
const error =
event.reason || (event.detail && event.detail.reason) || event;
const error = event.reason || event;
analytics.logException(error);
event.preventDefault();
},
@@ -216,8 +216,7 @@ function prepareDrive(drive: Drive) {
disabled: true,
icon: 'warning',
size: null,
link:
'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
linkCTA: 'Install',
linkTitle: 'Install missing drivers',
linkMessage: outdent`
@@ -231,12 +230,12 @@ function prepareDrive(drive: Drive) {
}
}
function setDrives(drives: _.Dictionary<any>) {
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
availableDrives.setDrives(_.values(drives));
}
function getDrives() {
return _.keyBy(availableDrives.getDrives() || [], 'device');
return _.keyBy(availableDrives.getDrives(), 'device');
}
async function addDrive(drive: Drive) {
@@ -250,6 +249,15 @@ async function addDrive(drive: Drive) {
}
function removeDrive(drive: Drive) {
if (
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isSourceDrive(drive.drive, getImage())
) {
// Deselect the image if it was on the drive that was removed.
// This will also deselect the image if the drive mountpoints change.
deselectImage();
}
const preparedDrive = prepareDrive(drive);
const drives = getDrives();
delete drives[preparedDrive.device];
@@ -264,7 +272,8 @@ function updateDriveProgress(
// @ts-ignore
const driveInMap = drives[drive.device];
if (driveInMap) {
driveInMap.progress = progress;
// @ts-ignore
drives[drive.device] = { ...driveInMap, progress };
setDrives(drives);
}
}
@@ -324,17 +333,31 @@ window.addEventListener('beforeunload', async (event) => {
flashingWorkflowUuid,
});
popupExists = false;
} catch (error) {
} catch (error: any) {
exceptionReporter.report(error);
}
});
async function main() {
await ledsInit();
export async function main() {
try {
const { init: ledsInit } = require('./models/leds');
await ledsInit();
} catch (error: any) {
exceptionReporter.report(error);
}
ReactDOM.render(
React.createElement(MainPage),
document.getElementById('main'),
// callback to set the correct zoomFactor for webviews as well
async () => {
const fullscreen = await settings.get('fullscreen');
const width = fullscreen ? window.screen.width : window.outerWidth;
try {
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
} catch (err) {
// noop
}
},
);
}
main();

View File

@@ -1,280 +0,0 @@
/*
* Copyright 2019 balena.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 { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash';
import * as React from 'react';
import { Modal } from 'rendition';
import {
COMPATIBILITY_STATUS_TYPES,
getDriveImageCompatibilityStatuses,
hasListDriveImageCompatibilityStatus,
isDriveValid,
} from '../../../../shared/drive-constraints';
import { bytesToClosestUnit } from '../../../../shared/units';
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
import * as selectionState from '../../models/selection-state';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
/**
* @summary Determine if we can change a drive's selection state
*/
function shouldChangeDriveSelectionState(drive: DrivelistDrive) {
return isDriveValid(drive, selectionState.getImage());
}
/**
* @summary Toggle a drive selection
*/
function toggleDrive(drive: DrivelistDrive) {
const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive);
if (canChangeDriveSelectionState) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: selectionState.isDriveSelected(drive.device),
});
selectionState.toggleDrive(drive.device);
}
}
/**
* @summary Get a drive's compatibility status object(s)
*
* @description
* Given a drive, return its compatibility status with the selected image,
* containing the status type (ERROR, WARNING), and accompanying
* status message.
*/
function getDriveStatuses(
drive: DrivelistDrive,
): Array<{ type: number; message: string }> {
return getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
}
function keyboardToggleDrive(
drive: DrivelistDrive,
event: React.KeyboardEvent<HTMLDivElement>,
) {
const ENTER = 13;
const SPACE = 32;
if (_.includes([ENTER, SPACE], event.keyCode)) {
toggleDrive(drive);
}
}
interface DriverlessDrive {
link: string;
linkTitle: string;
linkMessage: string;
}
export function DriveSelectorModal({ close }: { close: () => void }) {
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const [missingDriversModal, setMissingDriversModal] = React.useState(
defaultMissingDriversModalState,
);
const [drives, setDrives] = React.useState(getDrives());
React.useEffect(() => {
const unsubscribe = store.subscribe(() => {
setDrives(getDrives());
});
return unsubscribe;
});
/**
* @summary Prompt the user to install missing usbboot drivers
*/
function installMissingDrivers(drive: {
link: string;
linkTitle: string;
linkMessage: string;
}) {
if (drive.link) {
analytics.logEvent('Open driver link modal', {
url: drive.link,
});
setMissingDriversModal({ drive });
}
}
/**
* @summary Select a drive and close the modal
*/
async function selectDriveAndClose(drive: DrivelistDrive) {
const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(
drive,
);
if (canChangeDriveSelectionState) {
selectionState.selectDrive(drive.device);
analytics.logEvent('Drive selected (double click)');
close();
}
}
const hasStatus = hasListDriveImageCompatibilityStatus(
selectionState.getSelectedDrives(),
selectionState.getImage(),
);
return (
<Modal
className="modal-drive-selector-modal"
titleElement="Select a Drive"
done={close}
action="Continue"
primaryButtonProps={{
primary: !hasStatus,
warning: hasStatus,
}}
>
<ul
style={{
height: '210px',
overflowX: 'hidden',
overflowY: 'auto',
padding: '0px',
}}
>
{_.map(drives, (drive, index) => {
return (
<li
key={`item-${drive.displayName}`}
className="list-group-item"
// @ts-ignore (FIXME: not a valid <li> attribute but used by css rule)
disabled={!isDriveValid(drive, selectionState.getImage())}
onDoubleClick={() => selectDriveAndClose(drive)}
onClick={() => toggleDrive(drive)}
>
{drive.icon && (
<img
className="list-group-item-section"
alt="Drive device type logo"
src={`media/${drive.icon}.svg`}
width="25"
height="30"
/>
)}
<div
className="list-group-item-section list-group-item-section-expanded"
tabIndex={15 + index}
onKeyPress={(evt) => keyboardToggleDrive(drive, evt)}
>
<h6 className="list-group-item-heading">
{drive.description}
{drive.size && (
<span className="word-keep">
{' '}
- {bytesToClosestUnit(drive.size)}
</span>
)}
</h6>
{!drive.link && (
<p className="list-group-item-text">{drive.displayName}</p>
)}
{drive.link && (
<p className="list-group-item-text">
{drive.displayName} -{' '}
<b>
<a onClick={() => installMissingDrivers(drive)}>
{drive.linkCTA}
</a>
</b>
</p>
)}
<footer className="list-group-item-footer">
{_.map(getDriveStatuses(drive), (status, idx) => {
const className = {
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger',
};
return (
<span
key={`${drive.displayName}-status-${idx}`}
className={`label ${className[status.type]}`}
>
{status.message}
</span>
);
})}
</footer>
{Boolean(drive.progress) && (
<progress
className="drive-init-progress"
value={drive.progress}
max="100"
></progress>
)}
</div>
{isDriveValid(drive, selectionState.getImage()) && (
<span
className="list-group-item-section tick tick--success"
// @ts-ignore (FIXME: not a valid <span> attribute but used by css rule)
disabled={!selectionState.isDriveSelected(drive.device)}
></span>
)}
</li>
);
})}
{!hasAvailableDrives() && (
<li className="list-group-item">
<div>
<b>Connect a drive!</b>
<div>No removable drive detected.</div>
</div>
</li>
)}
</ul>
{missingDriversModal.drive !== undefined && (
<Modal
width={400}
title={missingDriversModal.drive.linkTitle}
cancel={() => setMissingDriversModal({})}
done={() => {
try {
if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link);
}
} catch (error) {
analytics.logException(error);
} finally {
setMissingDriversModal({});
}
}}
action={'Yes, continue'}
cancelButtonProps={{
children: 'Cancel',
}}
children={
missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
}
></Modal>
)}
</Modal>
);
}

View File

@@ -0,0 +1,556 @@
/*
* Copyright 2019 balena.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 ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
import * as React from 'react';
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
import styled from 'styled-components';
import {
getDriveImageCompatibilityStatuses,
isDriveValid,
DriveStatus,
DrivelistDrive,
isDriveSizeLarge,
} from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes';
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
import { getImage, isDriveSelected } from '../../models/selection-state';
import { store } from '../../models/store';
import { logEvent, logException } from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import {
Alert,
GenericTableProps,
Modal,
Table,
} from '../../styled-components';
import { SourceMetadata } from '../source-selector/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis';
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number;
}
interface DriverlessDrive {
displayName: string; // added in app.ts
description: string;
link: string;
linkTitle: string;
linkMessage: string;
linkCTA: string;
}
type Drive = DrivelistDrive | DriverlessDrive | UsbbootDrive;
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
return (drive as UsbbootDrive).progress !== undefined;
}
function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
return (drive as DriverlessDrive).link !== undefined;
}
function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
return typeof (drive as DrivelistDrive).size === 'number';
}
const DrivesTable = styled((props: GenericTableProps<Drive>) => (
<Table<Drive> {...props} />
))`
[data-display='table-head'],
[data-display='table-body'] {
> [data-display='table-row'] > [data-display='table-cell'] {
&:nth-child(2) {
width: 32%;
}
&:nth-child(3) {
width: 15%;
}
&:nth-child(4) {
width: 15%;
}
&:nth-child(5) {
width: 32%;
}
}
}
`;
function badgeShadeFromStatus(status: string) {
switch (status) {
case compatibility.containsImage():
return 16;
case compatibility.system():
case compatibility.tooSmall():
return 5;
default:
return 14;
}
}
const InitProgress = styled(
({
value,
...props
}: {
value: number;
props?: React.ProgressHTMLAttributes<Element>;
}) => {
return <progress max="100" value={value} {...props} />;
},
)`
/* Reset the default appearance */
appearance: none;
::-webkit-progress-bar {
width: 130px;
height: 4px;
background-color: #dde1f0;
border-radius: 14px;
}
::-webkit-progress-value {
background-color: #1496e1;
border-radius: 14px;
}
`;
export interface DriveSelectorProps
extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
write: boolean;
multipleSelection: boolean;
showWarnings?: boolean;
cancel: (drives: DrivelistDrive[]) => void;
done: (drives: DrivelistDrive[]) => void;
titleLabel: string;
emptyListLabel: string;
emptyListIcon: JSX.Element;
selectedList?: DrivelistDrive[];
updateSelectedList?: () => DrivelistDrive[];
onSelect?: (drive: DrivelistDrive) => void;
}
interface DriveSelectorState {
drives: Drive[];
image?: SourceMetadata;
missingDriversModal: { drive?: DriverlessDrive };
selectedList: DrivelistDrive[];
showSystemDrives: boolean;
}
function isSystemDrive(drive: Drive) {
return isDrivelistDrive(drive) && drive.isSystem;
}
export class DriveSelector extends React.Component<
DriveSelectorProps,
DriveSelectorState
> {
private unsubscribe: (() => void) | undefined;
tableColumns: Array<TableColumn<Drive>>;
originalList: DrivelistDrive[];
constructor(props: DriveSelectorProps) {
super(props);
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const selectedList = this.props.selectedList || [];
this.originalList = [...(this.props.selectedList || [])];
this.state = {
drives: getDrives(),
image: getImage(),
missingDriversModal: defaultMissingDriversModalState,
selectedList,
showSystemDrives: false,
};
this.tableColumns = [
{
field: 'description',
label: 'Name',
render: (description: string, drive: Drive) => {
if (isDrivelistDrive(drive)) {
const isLargeDrive = isDriveSizeLarge(drive);
const hasWarnings =
this.props.showWarnings && (isLargeDrive || drive.isSystem);
return (
<Flex alignItems="center">
{hasWarnings && (
<ExclamationTriangleSvg
height="1em"
fill={drive.isSystem ? '#fca321' : '#8f9297'}
/>
)}
<Txt ml={(hasWarnings && 8) || 0}>
{middleEllipsis(description, 32)}
</Txt>
</Flex>
);
}
return <Txt>{description}</Txt>;
},
},
{
field: 'description',
key: 'size',
label: 'Size',
render: (_description: string, drive: Drive) => {
if (isDrivelistDrive(drive) && drive.size !== null) {
return prettyBytes(drive.size);
}
},
},
{
field: 'description',
key: 'link',
label: 'Location',
render: (_description: string, drive: Drive) => {
return (
<Txt>
{drive.displayName}
{isDriverlessDrive(drive) && (
<>
{' '}
-{' '}
<b>
<a onClick={() => this.installMissingDrivers(drive)}>
{drive.linkCTA}
</a>
</b>
</>
)}
</Txt>
);
},
},
{
field: 'description',
key: 'extra',
// We use an empty React fragment otherwise it uses the field name as label
label: <></>,
render: (_description: string, drive: Drive) => {
if (isUsbbootDrive(drive)) {
return this.renderProgress(drive.progress);
} else if (isDrivelistDrive(drive)) {
return this.renderStatuses(drive);
}
},
},
];
}
private driveShouldBeDisabled(drive: Drive, image?: SourceMetadata) {
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
!isDriveValid(drive, image, this.props.write) ||
(this.props.write && drive.isReadOnly)
);
}
private getDisplayedDrives(drives: Drive[]): Drive[] {
return drives.filter((drive) => {
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
isDriveSelected(drive.device) ||
this.state.showSystemDrives ||
!drive.isSystem
);
});
}
private getDisabledDrives(drives: Drive[], image?: SourceMetadata): string[] {
return drives
.filter((drive) => this.driveShouldBeDisabled(drive, image))
.map((drive) => drive.displayName);
}
private renderProgress(progress: number) {
return (
<Flex flexDirection="column">
<Txt fontSize={12}>Initializing device</Txt>
<InitProgress value={progress} />
</Flex>
);
}
private warningFromStatus(
status: string,
drive: { device: string; size: number },
) {
switch (status) {
case compatibility.containsImage():
return warning.sourceDrive();
case compatibility.largeDrive():
return warning.largeDriveSize();
case compatibility.system():
return warning.systemDrive();
case compatibility.tooSmall():
const size =
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
return warning.tooSmall({ size }, drive);
}
}
private renderStatuses(drive: DrivelistDrive) {
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
drive,
this.state.image,
this.props.write,
).slice(0, 2);
return (
// the column render fn expects a single Element
<>
{statuses.map((status) => {
const badgeShade = badgeShadeFromStatus(status.message);
const warningMessage = this.warningFromStatus(status.message, {
device: drive.device,
size: drive.size || 0,
});
return (
<Badge
key={status.message}
shade={badgeShade}
mr="8px"
tooltip={this.props.showWarnings ? warningMessage : ''}
>
{status.message}
</Badge>
);
})}
</>
);
}
private installMissingDrivers(drive: DriverlessDrive) {
if (drive.link) {
logEvent('Open driver link modal', {
url: drive.link,
});
this.setState({ missingDriversModal: { drive } });
}
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
const drives = getDrives();
const image = getImage();
this.setState({
drives,
image,
selectedList:
(this.props.updateSelectedList && this.props.updateSelectedList()) ||
[],
});
});
}
componentWillUnmount() {
this.unsubscribe?.();
}
render() {
const { cancel, done, ...props } = this.props;
const { selectedList, drives, image, missingDriversModal } = this.state;
const displayedDrives = this.getDisplayedDrives(drives);
const disabledDrives = this.getDisabledDrives(drives, image);
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
const numberOfDisplayedSystemDrives =
displayedDrives.filter(isSystemDrive).length;
const numberOfHiddenSystemDrives =
numberOfSystemDrives - numberOfDisplayedSystemDrives;
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
const showWarnings = this.props.showWarnings && hasSystemDrives;
return (
<Modal
titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left">
{this.props.titleLabel}
</Txt>
<Txt
fontSize={11}
ml={12}
color="#5b82a7"
style={{ fontWeight: 600 }}
>
{drives.length} found
</Txt>
</Flex>
}
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={() => cancel(this.originalList)}
done={() => done(selectedList)}
action={`Select (${selectedList.length})`}
primaryButtonProps={{
primary: !showWarnings,
warning: showWarnings,
disabled: !hasAvailableDrives(),
}}
{...props}
>
{!hasAvailableDrives() ? (
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
width="100%"
>
{this.props.emptyListIcon}
<b>{this.props.emptyListLabel}</b>
</Flex>
) : (
<>
<DrivesTable
refFn={(t) => {
if (t !== null) {
t.setRowSelection(selectedList);
}
}}
checkedRowsNumber={selectedList.length}
multipleSelection={this.props.multipleSelection}
columns={this.tableColumns}
data={displayedDrives}
disabledRows={disabledDrives}
getRowClass={(row: Drive) =>
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
}
rowKey="displayName"
onCheck={(rows: Drive[]) => {
let newSelection = rows.filter(isDrivelistDrive);
if (this.props.multipleSelection) {
if (rows.length === 0) {
newSelection = [];
}
const deselecting = selectedList.filter(
(selected) =>
newSelection.filter(
(row) => row.device === selected.device,
).length === 0,
);
const selecting = newSelection.filter(
(row) =>
selectedList.filter(
(selected) => row.device === selected.device,
).length === 0,
);
deselecting.concat(selecting).forEach((row) => {
if (this.props.onSelect) {
this.props.onSelect(row);
}
});
this.setState({
selectedList: newSelection,
});
return;
}
if (this.props.onSelect) {
this.props.onSelect(newSelection[newSelection.length - 1]);
}
this.setState({
selectedList: newSelection.slice(newSelection.length - 1),
});
}}
onRowClick={(row: Drive) => {
if (
!isDrivelistDrive(row) ||
this.driveShouldBeDisabled(row, image)
) {
return;
}
if (this.props.onSelect) {
this.props.onSelect(row);
}
const index = selectedList.findIndex(
(d) => d.device === row.device,
);
const newList = this.props.multipleSelection
? [...selectedList]
: [];
if (index === -1) {
newList.push(row);
} else {
// Deselect if selected
newList.splice(index, 1);
}
this.setState({
selectedList: newList,
});
}}
/>
{numberOfHiddenSystemDrives > 0 && (
<Link
mt={15}
mb={15}
fontSize="14px"
onClick={() => this.setState({ showSystemDrives: true })}
>
<Flex alignItems="center">
<ChevronDownSvg height="1em" fill="currentColor" />
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
</Flex>
</Link>
)}
</>
)}
{this.props.showWarnings && hasSystemDrives ? (
<Alert className="system-drive-alert" style={{ width: '67%' }}>
Selecting your system drive is dangerous and will erase your drive!
</Alert>
) : null}
{missingDriversModal.drive !== undefined && (
<Modal
width={400}
title={missingDriversModal.drive.linkTitle}
cancel={() => this.setState({ missingDriversModal: {} })}
done={() => {
try {
if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link);
}
} catch (error: any) {
logException(error);
} finally {
this.setState({ missingDriversModal: {} });
}
}}
action="Yes, continue"
cancelButtonProps={{
children: 'Cancel',
}}
children={
missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
}
/>
)}
</Modal>
);
}
}

View File

@@ -1,113 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
.modal-drive-selector-modal .modal-content {
width: 315px;
height: 320px;
}
.modal-drive-selector-modal .modal-body {
padding-top: 0;
padding-bottom: 0;
}
.modal-drive-selector-modal .list-group-item[disabled] {
cursor: not-allowed;
}
.modal-drive-selector-modal {
.list-group-item-footer:has(span) {
margin-top: 8px;
}
.list-group-item-heading,
.list-group-item-text {
word-break: break-all;
}
.list-group {
margin-bottom: 0;
}
.list-group-item {
display: flex;
align-items: center;
border-left: 0;
border-right: 0;
border-radius: 0;
border-color: darken($palette-theme-light-background, 7%);
padding: 12px 0;
.list-group-item-section-expanded {
flex-grow: 1;
margin-left: 15px;
}
.list-group-item-section + .list-group-item-section {
margin-left: 10px;
display: inline-block;
vertical-align: middle;
}
> .tick {
font-size: 11px;
}
&:first-child {
border-top: 0;
}
&[disabled] .list-group-item-heading {
color: $palette-theme-light-soft-foreground;
}
.drive-init-progress {
appearance: none;
width: 100%;
height: 2.5px;
border: none;
border-radius: 50% 50%;
}
.drive-init-progress::-webkit-progress-bar {
background-color: $palette-theme-default-background;
border: none;
outline: none;
}
.drive-init-progress::-webkit-progress-value {
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
background-color: $palette-theme-primary-background;
}
}
.list-group-item-heading {
font-size: 13px;
}
.list-group-item-text {
line-height: 1;
font-size: 11px;
color: $palette-theme-light-soft-foreground;
}
.word-keep {
word-break: keep-all;
}
}

View File

@@ -0,0 +1,82 @@
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import * as _ from 'lodash';
import * as React from 'react';
import { Badge, Flex, Txt, ModalProps } from 'rendition';
import { Modal, ScrollableFlex } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as prettyBytes from 'pretty-bytes';
import { DriveWithWarnings } from '../../pages/main/Flash';
const DriveStatusWarningModal = ({
done,
cancel,
isSystem,
drivesWithWarnings,
}: ModalProps & {
isSystem: boolean;
drivesWithWarnings: DriveWithWarnings[];
}) => {
let warningSubtitle = 'You are about to erase an unusually large drive';
let warningCta = 'Are you sure the selected drive is not a storage drive?';
if (isSystem) {
warningSubtitle = "You are about to erase your computer's drives";
warningCta = 'Are you sure you want to flash your system drive?';
}
return (
<Modal
footerShadow={false}
reverseFooterButtons={true}
done={done}
cancel={cancel}
cancelButtonProps={{
primary: false,
warning: true,
children: 'Change target',
}}
action={"Yes, I'm sure"}
primaryButtonProps={{
primary: false,
outline: true,
}}
>
<Flex
flexDirection="column"
alignItems="center"
justifyContent="center"
width="100%"
>
<Flex flexDirection="column">
<ExclamationTriangleSvg height="2em" fill="#fca321" />
<Txt fontSize="24px" color="#fca321">
WARNING!
</Txt>
</Flex>
<Txt fontSize="24px">{warningSubtitle}</Txt>
<ScrollableFlex
flexDirection="column"
backgroundColor="#fff5e6"
m="2em 0"
p="1em 2em"
width="420px"
maxHeight="100px"
>
{drivesWithWarnings.map((drive, i, array) => (
<>
<Flex justifyContent="space-between" alignItems="baseline">
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
{drive.size && prettyBytes(drive.size) + ' '}
<Badge shade={5}>{drive.statuses[0].message}</Badge>
</Flex>
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}
</>
))}
</ScrollableFlex>
<Txt style={{ fontWeight: 600 }}>{warningCta}</Txt>
</Flex>
</Modal>
);
};
export default DriveStatusWarningModal;

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2016 balena.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 React from 'react';
import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics';
import { SafeWebview } from '../safe-webview/safe-webview';
interface FeaturedProjectProps {
onWebviewShow: (isWebviewShowing: boolean) => void;
}
interface FeaturedProjectState {
endpoint: string | null;
}
export class FeaturedProject extends React.Component<
FeaturedProjectProps,
FeaturedProjectState
> {
constructor(props: FeaturedProjectProps) {
super(props);
this.state = { endpoint: null };
}
public async componentDidMount() {
try {
const endpoint =
(await settings.get('featuredProjectEndpoint')) ||
'https://assets.balena.io/etcher-featured/index.html';
this.setState({ endpoint });
} catch (error) {
analytics.logException(error);
}
}
public render() {
return this.state.endpoint ? (
<SafeWebview src={this.state.endpoint} {...this.props}></SafeWebview>
) : null;
}
}

View File

@@ -14,18 +14,18 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import * as React from 'react';
import { Flex } from 'rendition';
import { v4 as uuidV4 } from 'uuid';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings';
import { Actions, store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import { FlashAnother } from '../flash-another/flash-another';
import { FlashResults } from '../flash-results/flash-results';
import { SVGIcon } from '../svg-icon/svg-icon';
import { FlashResults, FlashError } from '../flash-results/flash-results';
import { SafeWebview } from '../safe-webview/safe-webview';
function restart(goToMain: () => void) {
selectionState.deselectAllDrives();
@@ -40,75 +40,84 @@ function restart(goToMain: () => void) {
goToMain();
}
function formattedErrors() {
const errors = _.map(
_.get(flashState.getFlashResults(), ['results', 'errors']),
(error) => {
return `${error.device}: ${error.message || error.code}`;
},
async function getSuccessBannerURL() {
return (
(await settings.get('successBannerURL')) ??
'https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true'
);
return errors.join('\n');
}
function FinishPage({ goToMain }: { goToMain: () => void }) {
const results = flashState.getFlashResults().results || {};
const [webviewShowing, setWebviewShowing] = React.useState(false);
const [successBannerURL, setSuccessBannerURL] = React.useState('');
(async () => {
setSuccessBannerURL(await getSuccessBannerURL());
})();
const flashResults = flashState.getFlashResults();
const errors: FlashError[] = (
store.getState().toJS().failedDeviceErrors || []
).map(([, error]: [string, FlashError]) => ({
...error,
}));
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
flashState.getFlashState();
const {
skip,
results = {
bytesWritten,
sourceMetadata: {
size,
blockmappedSize,
},
averageFlashingSpeed: averageSpeed,
devices: { failed, successful: 0 },
},
} = flashResults;
return (
<div className="page-finish row around-xs">
<div className="col-xs">
<div className="box center">
<FlashResults results={results} errors={formattedErrors()} />
<Flex height="100%" justifyContent="space-between">
<Flex
width={webviewShowing ? '36.2vw' : '100vw'}
height="100vh"
alignItems="center"
justifyContent="center"
flexDirection="column"
style={{
position: 'absolute',
top: 0,
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<FlashResults
image={selectionState.getImage()?.name}
results={results}
skip={skip}
errors={errors}
mb="32px"
goToMain={goToMain}
/>
<FlashAnother
onClick={() => {
restart(goToMain);
}}
/>
</div>
<div className="box center">
<div className="fallback-banner">
<div className="caption-big">
Thanks for using
<span
style={{ cursor: 'pointer' }}
onClick={() =>
openExternal(
'https://balena.io/etcher?ref=etcher_offline_banner',
)
}
>
<SVGIcon
paths={['etcher.svg']}
width="165px"
height="auto"
></SVGIcon>
</span>
</div>
<div className="caption-small fallback-footer">
made with
<SVGIcon
paths={['love.svg']}
width="auto"
height="20px"
></SVGIcon>
by
<span
style={{ cursor: 'pointer' }}
onClick={() =>
openExternal('https://balena.io?ref=etcher_success')
}
>
<SVGIcon
paths={['balena.svg']}
width="auto"
height="20px"
></SVGIcon>
</span>
</div>
</div>
</div>
</div>
</div>
<FlashAnother
onClick={() => {
restart(goToMain);
}}
/>
</Flex>
{successBannerURL.length && (
<SafeWebview
src={successBannerURL}
onWebviewShow={setWebviewShowing}
style={{
display: webviewShowing ? 'flex' : 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
</Flex>
);
}

View File

@@ -15,24 +15,17 @@
*/
import * as React from 'react';
import styled from 'styled-components';
import { BaseButton } from '../../styled-components';
const FlashAnotherButton = styled(BaseButton)`
position: absolute;
right: 152px;
top: 60px;
`;
export interface FlashAnotherProps {
onClick: () => void;
}
export const FlashAnother = (props: FlashAnotherProps) => {
return (
<FlashAnotherButton primary onClick={props.onClick}>
Flash Another
</FlashAnotherButton>
<BaseButton primary onClick={props.onClick}>
Flash another
</BaseButton>
);
};

View File

@@ -14,80 +14,186 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
import outdent from 'outdent';
import * as React from 'react';
import { Txt } from 'rendition';
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
import styled from 'styled-components';
import { left, position, space, top } from 'styled-system';
import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units';
import { Underline } from '../../styled-components';
const Div = styled.div<any>`
${position}
${top}
${left}
${space}
import FlashSvg from '../../../assets/flash.svg';
import { getDrives } from '../../models/available-drives';
import { resetState } from '../../models/flash-state';
import * as selection from '../../models/selection-state';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { Modal, Table } from '../../styled-components';
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
&&& [data-display='table-head'],
&&& [data-display='table-body'] {
> [data-display='table-row'] {
> [data-display='table-cell'] {
&:first-child {
width: 30%;
}
&:nth-child(2) {
width: 20%;
}
&:last-child {
width: 50%;
}
}
}
}
`;
const DoneIcon = (props: {
skipped: boolean;
color: string;
allFailed: boolean;
}) => {
const svgProps = {
width: '28px',
fill: props.color,
style: {
marginTop: '-25px',
marginLeft: '13px',
zIndex: 1,
},
};
return props.allFailed && !props.skipped ? (
<TimesCircleSvg {...svgProps} />
) : (
<CheckCircleSvg {...svgProps} />
);
};
export interface FlashError extends Error {
description: string;
device: string;
code: string;
}
function formattedErrors(errors: FlashError[]) {
return errors
.map((error) => `${error.device}: ${error.message || error.code}`)
.join('\n');
}
const columns: Array<TableColumn<FlashError>> = [
{
field: 'description',
label: 'Target',
},
{
field: 'device',
label: 'Location',
},
{
field: 'message',
label: 'Error',
render: (message: string, { code }: FlashError) => {
return message ?? code;
},
},
];
function getEffectiveSpeed(results: {
sourceMetadata: {
size: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
}) {
const flashedSize =
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
const timeSpent = flashedSize / results.averageFlashingSpeed;
return results.sourceMetadata.size / timeSpent;
}
export function FlashResults({
goToMain,
image = '',
errors,
results,
skip,
...props
}: {
errors: string;
goToMain: () => void;
image?: string;
errors: FlashError[];
skip: boolean;
results: {
bytesWritten: number;
sourceMetadata: {
size: number;
blockmappedSize: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
devices: { failed: number; successful: number };
};
}) {
const allDevicesFailed = results.devices.successful === 0;
const effectiveSpeed = _.round(
bytesToMegabytes(
results.sourceMetadata.size /
(results.bytesWritten / results.averageFlashingSpeed),
),
} & FlexProps) {
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = !skip && results.devices.successful === 0;
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
1,
);
return (
<Div position="absolute" left="153px" top="66px">
<div className="inline-flex title">
<span
className={`tick tick--${
allDevicesFailed ? 'error' : 'success'
} space-right-medium`}
></span>
<h3>Flash Complete!</h3>
</div>
<Div className="results" mr="0" mb="0" ml="40px">
{_.map(results.devices, (quantity, type) => {
return quantity ? (
<Underline
tooltip={type === 'failed' ? errors : undefined}
key={type}
>
<div
key={type}
className={`target-status-line target-status-${type}`}
>
<span className="target-status-dot"></span>
<span className="target-status-quantity">{quantity}</span>
<span className="target-status-message">
{progress[type](quantity)}
</span>
</div>
</Underline>
) : null;
})}
{!allDevicesFailed && (
<Flex flexDirection="column" {...props}>
<Flex alignItems="center" flexDirection="column">
<Flex
alignItems="center"
mt="50px"
mb="32px"
color="#7e8085"
flexDirection="column"
>
<FlashSvg width="40px" height="40px" className="disabled" />
<DoneIcon
skipped={skip}
allFailed={allFailed}
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
/>
<Txt>{middleEllipsis(image, 24)}</Txt>
</Flex>
<Txt fontSize={24} color="#fff" mb="17px">
Flash {allFailed ? 'Failed' : 'Complete'}!
</Txt>
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
</Flex>
<Flex flexDirection="column" color="#7e8085">
{results.devices.successful !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#1ac135" />
<Txt ml="10px" color="#fff">
{results.devices.successful}
</Txt>
<Txt ml="10px">
{progress.successful(results.devices.successful)}
</Txt>
</Flex>
) : null}
{errors.length !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#ff4444" />
<Txt ml="10px" color="#fff">
{errors.length}
</Txt>
<Txt ml="10px" tooltip={formattedErrors(errors)}>
{progress.failed(errors.length)}
</Txt>
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
more info
</Link>
</Flex>
) : null}
{!allFailed && (
<Txt
color="#787c7f"
fontSize="10px"
style={{
fontWeight: 500,
@@ -101,7 +207,37 @@ export function FlashResults({
Effective speed: {effectiveSpeed} MB/s
</Txt>
)}
</Div>
</Div>
</Flex>
{showErrorsInfo && (
<Modal
titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left">
Failed targets
</Txt>
</Flex>
}
action="Retry failed targets"
cancel={() => setShowErrorsInfo(false)}
done={() => {
setShowErrorsInfo(false);
resetState();
getDrives()
.map((drive) => {
selection.deselectDrive(drive.device);
return drive.device;
})
.filter((driveDevice) =>
errors.some((error) => error.device === driveDevice),
)
.forEach((driveDevice) => selection.selectDrive(driveDevice));
goToMain();
}}
>
<ErrorsTable columns={columns} data={errors} />
</Modal>
)}
</Flex>
);
}

View File

@@ -15,7 +15,7 @@
*/
import * as React from 'react';
import { Button, Flex, ProgressBar, Txt } from 'rendition';
import { Flex, Button, ProgressBar, Txt } from 'rendition';
import { default as styled } from 'styled-components';
import { fromFlashState } from '../../modules/progress-status';
@@ -23,7 +23,7 @@ import { StepButton } from '../../styled-components';
const FlashProgressBar = styled(ProgressBar)`
> div {
width: 220px;
width: 100%;
height: 12px;
color: white !important;
text-shadow: none !important;
@@ -33,8 +33,9 @@ const FlashProgressBar = styled(ProgressBar)`
}
}
width: 220px;
width: 100%;
height: 12px;
margin-bottom: 6px;
border-radius: 14px;
font-size: 16px;
line-height: 48px;
@@ -48,8 +49,9 @@ interface ProgressButtonProps {
percentage: number;
position: number;
disabled: boolean;
cancel: () => void;
cancel: (type: string) => void;
callback: () => void;
warning?: boolean;
}
const colors = {
@@ -58,11 +60,14 @@ const colors = {
verifying: '#1ac135',
} as const;
const CancelButton = styled((props) => (
<Button plain {...props}>
Cancel
</Button>
))`
const CancelButton = styled(({ type, onClick, ...props }) => {
const status = type === 'verifying' ? 'Skip' : 'Cancel';
return (
<Button plain onClick={() => onClick(status)} {...props}>
{status}
</Button>
);
})`
font-weight: 600;
&&& {
width: auto;
@@ -73,33 +78,53 @@ const CancelButton = styled((props) => (
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
public render() {
const percentage = this.props.percentage;
const warning = this.props.warning;
const { status, position } = fromFlashState({
type: this.props.type,
percentage,
position: this.props.position,
percentage: this.props.percentage,
});
const type = this.props.type || 'default';
if (this.props.active) {
return (
<div>
<Flex justifyContent="space-between" style={{ fontWeight: 600 }}>
<>
<Flex
alignItems="baseline"
justifyContent="space-between"
width="100%"
style={{
marginTop: 42,
marginBottom: '6px',
fontSize: 16,
fontWeight: 600,
}}
>
<Flex>
<Txt color="#fff">{status}&nbsp;</Txt>
<Txt color={colors[this.props.type]}>{position}</Txt>
<Txt color={colors[type]}>{position}</Txt>
</Flex>
<CancelButton onClick={this.props.cancel} color="#00aeef" />
{type && (
<CancelButton
type={type}
onClick={this.props.cancel}
color="#00aeef"
/>
)}
</Flex>
<FlashProgressBar
background={colors[this.props.type]}
value={this.props.percentage}
/>
</div>
<FlashProgressBar background={colors[type]} value={percentage} />
</>
);
}
return (
<StepButton
primary
primary={!warning}
warning={warning}
onClick={this.props.callback}
disabled={this.props.disabled}
style={{
marginTop: 30,
}}
>
Flash!
</StepButton>

View File

@@ -15,77 +15,60 @@
*/
import * as React from 'react';
import { default as styled } from 'styled-components';
import { color } from 'styled-system';
import { Flex, Txt } from 'rendition';
import DriveSvg from '../../../assets/drive.svg';
import ImageSvg from '../../../assets/image.svg';
import { SVGIcon } from '../svg-icon/svg-icon';
const Div = styled.div`
position: absolute;
top: 45px;
left: 545px;
> span.step-name {
justify-content: flex-start;
> span {
margin-left: 10px;
}
> span:nth-child(2) {
font-weight: 500;
}
> span:nth-child(3) {
font-weight: 400;
font-style: italic;
}
}
.svg-icon[disabled] {
opacity: 0.4;
}
`;
const Span = styled.span`
${color}
`;
import { middleEllipsis } from '../../utils/middle-ellipsis';
interface ReducedFlashingInfosProps {
imageLogo: string;
imageName: string;
imageLogo?: string;
imageName?: string;
imageSize: string;
driveTitle: string;
shouldShow: boolean;
driveLabel: string;
style?: React.CSSProperties;
}
export class ReducedFlashingInfos extends React.Component<
ReducedFlashingInfosProps
> {
export class ReducedFlashingInfos extends React.Component<ReducedFlashingInfosProps> {
constructor(props: ReducedFlashingInfosProps) {
super(props);
this.state = {};
}
public render() {
return this.props.shouldShow ? (
<Div>
<Span className="step-name">
const { imageName = '' } = this.props;
return (
<Flex
flexDirection="column"
style={this.props.style ? this.props.style : undefined}
>
<Flex mb={16}>
<SVGIcon
disabled
contents={[this.props.imageLogo]}
paths={['image.svg']}
width="20px"
></SVGIcon>
<Span>{this.props.imageName}</Span>
<Span color="#7e8085">{this.props.imageSize}</Span>
</Span>
width="21px"
height="21px"
contents={this.props.imageLogo}
fallback={ImageSvg}
style={{ marginRight: '9px' }}
/>
<Txt
style={{ marginRight: '9px' }}
tooltip={{ text: imageName, placement: 'right' }}
>
{middleEllipsis(imageName, 16)}
</Txt>
<Txt color="#7e8085">{this.props.imageSize}</Txt>
</Flex>
<Span className="step-name">
<SVGIcon disabled paths={['drive.svg']} width="20px"></SVGIcon>
<Span>{this.props.driveTitle}</Span>
</Span>
</Div>
) : null;
<Flex>
<DriveSvg width="21px" height="21px" style={{ marginRight: '9px' }} />
<Txt tooltip={{ text: this.props.driveLabel, placement: 'right' }}>
{middleEllipsis(this.props.driveTitle, 16)}
</Txt>
</Flex>
</Flex>
);
}
}

View File

@@ -58,10 +58,9 @@ const API_VERSION = '2';
interface SafeWebviewProps {
// The website source URL
src: string;
// @summary Refresh the webview
refreshNow?: boolean;
// Webview lifecycle event
onWebviewShow?: (isWebviewShowing: boolean) => void;
style?: React.CSSProperties;
}
interface SafeWebviewState {
@@ -109,15 +108,18 @@ export class SafeWebview extends React.PureComponent<
}
public render() {
const {
style = {
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
},
} = this.props;
return (
<webview
ref={this.webviewRef}
partition={ELECTRON_SESSION}
style={{
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
}}
style={style}
/>
);
}

View File

@@ -14,115 +14,50 @@
* limitations under the License.
*/
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
import * as _ from 'lodash';
import * as os from 'os';
import * as React from 'react';
import { Badge, Checkbox, Modal } from 'rendition';
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
import { version } from '../../../../../package.json';
import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
const platform = os.platform();
interface WarningModalProps {
message: string;
confirmLabel: string;
cancel: () => void;
done: () => void;
}
const WarningModal = ({
message,
confirmLabel,
cancel,
done,
}: WarningModalProps) => {
return (
<Modal
title={confirmLabel}
action={confirmLabel}
cancel={cancel}
done={done}
style={{
width: 420,
height: 300,
}}
primaryButtonProps={{ warning: true }}
>
{message}
</Modal>
);
};
import { Modal } from '../../styled-components';
interface Setting {
name: string;
label: string | JSX.Element;
options?: {
description: string;
confirmLabel: string;
};
hide?: boolean;
}
async function getSettingsList(): Promise<Setting[]> {
return [
const list: Setting[] = [
{
name: 'errorReporting',
label: 'Anonymously report errors and usage statistics to balena.io',
},
{
name: 'unmountOnSuccess',
/**
* On Windows, "Unmounting" basically means "ejecting".
* On top of that, Windows users are usually not even
* familiar with the meaning of "unmount", which comes
* from the UNIX world.
*/
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
},
{
name: 'validateWriteOnSuccess',
label: 'Validate write on success',
},
{
];
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
list.push({
name: 'updatesEnabled',
label: 'Auto-updates enabled',
},
{
name: 'unsafeMode',
label: (
<span>
Unsafe mode{' '}
<Badge danger fontSize={12}>
Dangerous
</Badge>
</span>
),
options: {
description: `Are you sure you want to turn this on?
You will be able to overwrite your system drives if you're not careful.`,
confirmLabel: 'Enable unsafe mode',
},
hide: await settings.get('disableUnsafeMode'),
},
];
}
interface Warning {
setting: string;
settingValue: boolean;
description: string;
confirmLabel: string;
});
}
return list;
}
interface SettingsModalProps {
toggleModal: (value: boolean) => void;
}
const UUID = process.env.BALENA_DEVICE_UUID;
const InfoBox = (props: any) => (
<Box fontSize={14}>
<Txt>{props.label}</Txt>
<TextWithCopy code text={props.value} copy={props.value} />
</Box>
);
export function SettingsModal({ toggleModal }: SettingsModalProps) {
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
React.useEffect(() => {
@@ -142,93 +77,69 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
}
})();
});
const [warning, setWarning] = React.useState<Warning | undefined>(undefined);
const toggleSetting = async (
setting: string,
options?: Setting['options'],
) => {
const toggleSetting = async (setting: string) => {
const value = currentSettings[setting];
const dangerous = options !== undefined;
analytics.logEvent('Toggle setting', {
setting,
value,
dangerous,
analytics.logEvent('Toggle setting', { setting, value });
await settings.set(setting, !value);
setCurrentSettings({
...currentSettings,
[setting]: !value,
});
if (value || options === undefined) {
await settings.set(setting, !value);
setCurrentSettings({
...currentSettings,
[setting]: !value,
});
setWarning(undefined);
return;
} else {
// Show warning since it's a dangerous setting
setWarning({
setting,
settingValue: value,
...options,
});
}
};
return (
<Modal
id="settings-modal"
title="Settings"
titleElement={
<Txt fontSize={24} mb={24}>
Settings
</Txt>
}
done={() => toggleModal(false)}
style={{
width: 780,
height: 420,
}}
>
<div>
{_.map(settingsList, (setting: Setting, i: number) => {
return setting.hide ? null : (
<div key={setting.name}>
<Flex flexDirection="column">
{settingsList.map((setting: Setting, i: number) => {
return (
<Flex key={setting.name} mb={14}>
<Checkbox
toggle
tabIndex={6 + i}
label={setting.label}
checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)}
onChange={() => toggleSetting(setting.name)}
/>
</div>
</Flex>
);
})}
<div>
<span
onClick={() =>
openExternal(
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
)
}
>
<FontAwesomeIcon icon={faGithub} /> {version}
</span>
</div>
</div>
{warning === undefined ? null : (
<WarningModal
message={warning.description}
confirmLabel={warning.confirmLabel}
done={async () => {
await settings.set(warning.setting, !warning.settingValue);
setCurrentSettings({
...currentSettings,
[warning.setting]: true,
});
setWarning(undefined);
{UUID !== undefined && (
<Flex flexDirection="column">
<Txt fontSize={24}>System Information</Txt>
<InfoBox label="UUID" value={UUID.substr(0, 7)} />
</Flex>
)}
<Flex
mt={18}
alignItems="center"
color="#00aeef"
style={{
width: 'fit-content',
cursor: 'pointer',
fontSize: 14,
}}
cancel={() => {
setWarning(undefined);
}}
/>
)}
onClick={() =>
openExternal(
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
)
}
>
<GithubSvg
height="1em"
fill="currentColor"
style={{ marginRight: 8 }}
/>
<Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
</Flex>
</Flex>
</Modal>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,13 +14,8 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as _ from 'lodash';
import * as path from 'path';
import * as React from 'react';
import * as analytics from '../../modules/analytics';
const domParser = new window.DOMParser();
const DEFAULT_SIZE = '40px';
@@ -28,115 +23,52 @@ const DEFAULT_SIZE = '40px';
/**
* @summary Try to parse SVG contents and return it data encoded
*
* @param {String} contents - SVG XML contents
* @returns {String|null}
*
* @example
* const encodedSVG = tryParseSVGContents('<svg><path></path></svg>')
*
* img.src = encodedSVG
*/
function tryParseSVGContents(contents: string) {
function tryParseSVGContents(contents?: string): string | undefined {
if (contents === undefined) {
return;
}
const doc = domParser.parseFromString(contents, 'image/svg+xml');
const parserError = doc.querySelector('parsererror');
const svg = doc.querySelector('svg');
if (!parserError && svg) {
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`;
}
return null;
}
interface SVGIconProps {
// Paths to SVG files to be tried in succession if any fails
paths: string[];
// List of embedded SVG contents to be tried in succession if any fails
contents?: string[];
// Optional string representing the SVG contents to be tried
contents?: string;
// Fallback SVG element to show if `contents` is invalid/undefined
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
// SVG image width unit
width?: string;
// SVG image height unit
height?: string;
// Should the element visually appear grayed out and disabled?
disabled?: boolean;
style?: React.CSSProperties;
}
/**
* @summary SVG element that takes both filepaths and file contents
* @summary SVG element that takes file contents
*/
export class SVGIcon extends React.Component<SVGIconProps> {
export class SVGIcon extends React.PureComponent<SVGIconProps> {
public render() {
// __dirname behaves strangely inside a Webpack bundle,
// so we need to provide different base directories
// depending on whether __dirname is absolute or not,
// which helps detecting a Webpack bundle.
// We use global.__dirname inside a Webpack bundle since
// that's the only way to get the "real" __dirname.
let baseDirectory: string;
if (path.isAbsolute(__dirname)) {
baseDirectory = path.join(__dirname);
} else {
// @ts-ignore
baseDirectory = global.__dirname;
const svgData = tryParseSVGContents(this.props.contents);
const { width, height, style = {} } = this.props;
style.width = width || DEFAULT_SIZE;
style.height = height || DEFAULT_SIZE;
if (svgData !== undefined) {
return (
<img
className={this.props.disabled ? 'disabled' : ''}
style={style}
src={svgData}
/>
);
}
let svgData = '';
_.find(this.props.contents, (content) => {
const attempt = tryParseSVGContents(content);
if (attempt) {
svgData = attempt;
return true;
}
return false;
});
if (!svgData) {
_.find(this.props.paths, (relativePath) => {
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(baseDirectory, 'media', relativePath);
const contents = _.attempt(() => {
return fs.readFileSync(imagePath, {
encoding: 'utf8',
});
});
if (_.isError(contents)) {
analytics.logException(contents);
return false;
}
const parsed = tryParseSVGContents(contents);
if (parsed) {
svgData = parsed;
return true;
}
return false;
});
}
const width = this.props.width || DEFAULT_SIZE;
const height = this.props.height || DEFAULT_SIZE;
return (
<img
className="svg-icon"
style={{
width,
height,
}}
src={svgData}
// @ts-ignore
disabled={this.props.disabled}
></img>
);
const { fallback: FallbackSVG } = this.props;
return <FallbackSVG style={style} />;
}
}

View File

@@ -14,18 +14,17 @@
* limitations under the License.
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import * as React from 'react';
import { Txt } from 'rendition';
import { default as styled } from 'styled-components';
import { Flex, FlexProps, Txt } from 'rendition';
import {
getDriveImageCompatibilityStatuses,
Image,
DriveStatus,
} from '../../../../shared/drive-constraints';
import { bytesToClosestUnit } from '../../../../shared/units';
import { getSelectedDrives } from '../../models/selection-state';
import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import {
ChangeButton,
DetailsText,
@@ -34,10 +33,6 @@ import {
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
const TargetDetail = styled((props) => <Txt.span {...props}></Txt.span>)`
float: ${({ float }) => float};
`;
interface TargetSelectorProps {
targets: any[];
disabled: boolean;
@@ -46,38 +41,56 @@ interface TargetSelectorProps {
flashing: boolean;
show: boolean;
tooltip: string;
image: Image;
}
function DriveCompatibilityWarning(props: {
drive: DrivelistDrive;
image: Image;
}) {
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
props.drive,
props.image,
);
if (compatibilityWarnings.length === 0) {
return null;
function getDriveWarning(status: DriveStatus) {
switch (status.message) {
case compatibility.containsImage():
return warning.sourceDrive();
case compatibility.largeDrive():
return warning.largeDriveSize();
case compatibility.system():
return warning.systemDrive();
default:
return '';
}
const messages = _.map(compatibilityWarnings, 'message');
return (
<Txt.span
className="glyphicon glyphicon-exclamation-sign"
ml={2}
tooltip={messages.join(', ')}
/>
);
}
export function TargetSelector(props: TargetSelectorProps) {
const DriveCompatibilityWarning = ({
warnings,
...props
}: {
warnings: string[];
} & FlexProps) => {
const systemDrive = warnings.find(
(message) => message === warning.systemDrive(),
);
return (
<Flex tooltip={warnings.join(', ')} {...props}>
<ExclamationTriangleSvg
fill={systemDrive ? '#fca321' : '#8f9297'}
height="1em"
/>
</Flex>
);
};
export function TargetSelectorButton(props: TargetSelectorProps) {
const targets = getSelectedDrives();
if (targets.length === 1) {
const target = targets[0];
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
{warnings.length > 0 && (
<DriveCompatibilityWarning warnings={warnings} mr={2} />
)}
{middleEllipsis(target.description, 20)}
</StepNameButton>
{!props.flashing && (
@@ -85,10 +98,9 @@ export function TargetSelector(props: TargetSelectorProps) {
Change
</ChangeButton>
)}
<DetailsText>
<DriveCompatibilityWarning drive={target} image={props.image} />
{bytesToClosestUnit(target.size)}
</DetailsText>
{target.size != null && (
<DetailsText>{prettyBytes(target.size)}</DetailsText>
)}
</>
);
}
@@ -96,29 +108,30 @@ export function TargetSelector(props: TargetSelectorProps) {
if (targets.length > 1) {
const targetsTemplate = [];
for (const target of targets) {
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
targetsTemplate.push(
<DetailsText
key={target.device}
tooltip={`${target.description} ${
target.displayName
} ${bytesToClosestUnit(target.size)}`}
tooltip={`${target.description} ${target.displayName} ${
target.size != null ? prettyBytes(target.size) : ''
}`}
px={21}
>
<Txt.span>
<DriveCompatibilityWarning drive={target} image={props.image} />
<TargetDetail float="left">
{middleEllipsis(target.description, 14)}
</TargetDetail>
<TargetDetail float="right">
{bytesToClosestUnit(target.size)}
</TargetDetail>
</Txt.span>
{warnings.length > 0 ? (
<DriveCompatibilityWarning warnings={warnings} mr={2} />
) : null}
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
</DetailsText>,
);
}
return (
<>
<StepNameButton plain tooltip={props.tooltip} fontSize={16}>
<StepNameButton plain tooltip={props.tooltip}>
{targets.length} Targets
</StepNameButton>
{!props.flashing && (

View File

@@ -0,0 +1,193 @@
/*
* Copyright 2016 balena.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 React from 'react';
import { Flex, Txt } from 'rendition';
import {
DriveSelector,
DriveSelectorProps,
} from '../drive-selector/drive-selector';
import {
isDriveSelected,
getImage,
getSelectedDrives,
deselectDrive,
selectDrive,
deselectAllDrives,
} from '../../models/selection-state';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { TargetSelectorButton } from './target-selector-button';
import TgtSvg from '../../../assets/tgt.svg';
import DriveSvg from '../../../assets/drive.svg';
import { warning } from '../../../../shared/messages';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
export const getDriveListLabel = () => {
return getSelectedDrives()
.map((drive: any) => {
return `${drive.description} (${drive.displayName})`;
})
.join('\n');
};
const getDriveSelectionStateSlice = () => ({
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
});
export const TargetSelectorModal = (
props: Omit<
DriveSelectorProps,
'titleLabel' | 'emptyListLabel' | 'multipleSelection' | 'emptyListIcon'
>,
) => (
<DriveSelector
multipleSelection={true}
titleLabel="Select target"
emptyListLabel="Plug a target drive"
emptyListIcon={<TgtSvg width="40px" />}
showWarnings={true}
selectedList={getSelectedDrives()}
updateSelectedList={getSelectedDrives}
{...props}
/>
);
export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
const selectedDrivesFromState = getSelectedDrives();
const deselected = selectedDrivesFromState.filter(
(drive) =>
!modalTargets.find((modalTarget) => modalTarget.device === drive.device),
);
// deselect drives
deselected.forEach((drive) => {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: true,
});
deselectDrive(drive.device);
});
// select drives
modalTargets.forEach((drive) => {
// Don't send events for drives that were already selected
if (!isDriveSelected(drive.device)) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: false,
});
}
selectDrive(drive.device);
});
};
interface TargetSelectorProps {
disabled: boolean;
hasDrive: boolean;
flashing: boolean;
}
export const TargetSelector = ({
disabled,
hasDrive,
flashing,
}: TargetSelectorProps) => {
// TODO: inject these from redux-connector
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
getDriveSelectionStateSlice(),
);
const [showTargetSelectorModal, setShowTargetSelectorModal] =
React.useState(false);
React.useEffect(() => {
return observe(() => {
setStateSlice(getDriveSelectionStateSlice());
});
}, []);
const hasSystemDrives = targets.some((target) => target.isSystem);
return (
<Flex flexDirection="column" alignItems="center">
<DriveSvg
className={disabled ? 'disabled' : ''}
width="40px"
style={{
marginBottom: 30,
}}
/>
<TargetSelectorButton
disabled={disabled}
show={!hasDrive}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowTargetSelectorModal(true);
}}
flashing={flashing}
targets={targets}
/>
{hasSystemDrives ? (
<Txt
color="#fca321"
style={{
position: 'absolute',
bottom: '25px',
}}
>
Warning: {warning.systemDrive()}
</Txt>
) : null}
{showTargetSelectorModal && (
<TargetSelectorModal
write={true}
cancel={(originalList) => {
if (originalList.length) {
selectAllTargets(originalList);
} else {
deselectAllDrives();
}
setShowTargetSelectorModal(false);
}}
done={(modalTargets) => {
if (modalTargets.length === 0) {
deselectAllDrives();
}
setShowTargetSelectorModal(false);
}}
onSelect={(drive) => {
if (
getSelectedDrives().find(
(selectedDrive) => selectedDrive.device === drive.device,
)
) {
return deselectDrive(drive.device);
}
selectDrive(drive.device);
}}
/>
)}
</Flex>
);
};

View File

@@ -14,40 +14,36 @@
* limitations under the License.
*/
/* Prevent text selection */
body {
-webkit-user-select: none;
@font-face {
font-family: "SourceSansPro";
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
/* Allow window to be dragged from anywhere */
#app-header {
-webkit-app-region: drag;
@font-face {
font-family: "SourceSansPro";
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
}
.modal-body {
-webkit-app-region: no-drag;
}
button,
a,
input {
-webkit-app-region: no-drag;
}
/* Prevent WebView bounce effect in OS X */
html,
body {
margin: 0;
overflow: hidden;
/* Prevent white flash when running application */
background-color: #4d5057;
/* Prevent WebView bounce effect in OS X */
height: 100%;
width: 100%;
}
html {
overflow: hidden;
}
/* Prevent text selection */
body {
overflow: hidden;
-webkit-user-select: none;
-webkit-overflow-scrolling: touch;
}
@@ -55,11 +51,16 @@ body {
a:focus,
input:focus,
button:focus,
[tabindex]:focus {
[tabindex]:focus,
input[type="checkbox"] + div {
outline: none !important;
box-shadow: none !important;
}
/* Titles don't have margins on desktop apps */
h1, h2, h3, h4, h5, h6 {
margin: 0;
.disabled {
opacity: 0.4;
}
#rendition-tooltip-root > div {
font-family: "SourceSansPro", sans-serif;
}

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>balenaEtcher</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<main id="main"></main>
<script src="http://localhost:3030/gui.js"></script>
</body>
</html>

View File

@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8">
<title>Etcher</title>
<title>balenaEtcher</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>

View File

@@ -14,21 +14,20 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import { DrivelistDrive } from '../../../shared/drive-constraints';
import { Actions, store } from './store';
export function hasAvailableDrives() {
return !_.isEmpty(getDrives());
return getDrives().length > 0;
}
export function setDrives(drives: any[]) {
store.dispatch({
type: Actions.SET_AVAILABLE_DRIVES,
type: Actions.SET_AVAILABLE_TARGETS,
data: drives,
});
}
export function getDrives(): any[] {
export function getDrives(): DrivelistDrive[] {
return store.getState().toJS().availableDrives;
}

View File

@@ -14,8 +14,10 @@
* limitations under the License.
*/
import * as electron from 'electron';
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import { DrivelistDrive } from '../../../shared/drive-constraints';
import { bytesToMegabytes } from '../../../shared/units';
import { Actions, store } from './store';
@@ -45,6 +47,8 @@ export function isFlashing(): boolean {
* start a flash process.
*/
export function setFlashingFlag() {
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.send('disable-screensaver');
store.dispatch({
type: Actions.SET_FLASHING_FLAG,
data: {},
@@ -66,6 +70,8 @@ export function unsetFlashingFlag(results: {
type: Actions.UNSET_FLASHING_FLAG,
data: results,
});
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.send('enable-screensaver');
}
export function setDevicePaths(devicePaths: string[]) {
@@ -75,14 +81,29 @@ export function setDevicePaths(devicePaths: string[]) {
});
}
export function addFailedDevicePath(devicePath: string) {
const failedDevicePathsSet = new Set(
store.getState().toJS().failedDevicePaths,
export function addFailedDeviceError({
device,
error,
}: {
device: DrivelistDrive;
error: Error;
}) {
const failedDeviceErrorsMap = new Map(
store.getState().toJS().failedDeviceErrors,
);
failedDevicePathsSet.add(devicePath);
if (failedDeviceErrorsMap.has(device.device)) {
// Only store the first error
return;
}
failedDeviceErrorsMap.set(device.device, {
description: device.description,
device: device.device,
devicePath: device.devicePath,
...error,
});
store.dispatch({
type: Actions.SET_FAILED_DEVICE_PATHS,
data: Array.from(failedDevicePathsSet),
type: Actions.SET_FAILED_DEVICE_ERRORS,
data: Array.from(failedDeviceErrorsMap),
});
}

View File

@@ -14,39 +14,20 @@
* limitations under the License.
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash';
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
import { isSourceDrive } from '../../../shared/drive-constraints';
import {
DrivelistDrive,
isSourceDrive,
} from '../../../shared/drive-constraints';
import { getDrives } from './available-drives';
import { getSelectedDrives } from './selection-state';
import * as settings from './settings';
import { DEFAULT_STATE, observe } from './store';
import { observe, store } from './store';
const leds: Map<string, RGBLed> = new Map();
function setLeds(
drivesPaths: Set<string>,
colorOrAnimation: Color | AnimationFunction,
frequency?: number,
) {
for (const path of drivesPaths) {
const led = leds.get(path);
if (led) {
if (Array.isArray(colorOrAnimation)) {
led.setStaticColor(colorOrAnimation);
} else {
led.setAnimation(colorOrAnimation, frequency);
}
}
}
}
const red: Color = [1, 0, 0];
const green: Color = [0, 1, 0];
const blue: Color = [0, 0, 1];
const white: Color = [1, 1, 1];
const black: Color = [0, 0, 0];
const purple: Color = [0.5, 0, 0.5];
const animator = new Animator([], 10);
function createAnimationFunction(
intensityFunction: (t: number) => number,
@@ -54,21 +35,39 @@ function createAnimationFunction(
): AnimationFunction {
return (t: number): Color => {
const intensity = intensityFunction(t);
return color.map((v) => v * intensity) as Color;
return color.map((v: number) => v * intensity) as Color;
};
}
function blink(t: number) {
return Math.floor(t / 1000) % 2;
return Math.floor(t) % 2;
}
function breathe(t: number) {
return (1 + Math.sin(t / 1000)) / 2;
function one(_t: number) {
return 1;
}
const breatheBlue = createAnimationFunction(breathe, blue);
const blinkGreen = createAnimationFunction(blink, green);
const blinkPurple = createAnimationFunction(blink, purple);
type LEDColors = {
green: Color;
purple: Color;
red: Color;
blue: Color;
white: Color;
black: Color;
};
type LEDAnimationFunctions = {
blinkGreen: AnimationFunction;
blinkPurple: AnimationFunction;
staticRed: AnimationFunction;
staticGreen: AnimationFunction;
staticBlue: AnimationFunction;
staticWhite: AnimationFunction;
staticBlack: AnimationFunction;
};
let ledColors: LEDColors;
let ledAnimationFunctions: LEDAnimationFunctions;
interface LedsState {
step: 'main' | 'flashing' | 'verifying' | 'finish';
@@ -78,6 +77,17 @@ interface LedsState {
failedDrives: string[];
}
function setLeds(animation: AnimationFunction, drivesPaths: Set<string>) {
const rgbLeds: RGBLed[] = [];
for (const path of drivesPaths) {
const led = leds.get(path);
if (led) {
rgbLeds.push(led);
}
}
return { animation, rgbLeds };
}
// Source slot (1st slot): behaves as a target unless it is chosen as source
// No drive: black
// Drive plugged: blue - on
@@ -108,6 +118,7 @@ export function updateLeds({
// Remove selected devices from plugged set
for (const d of selectedOk) {
plugged.delete(d);
unplugged.delete(d);
}
// Remove plugged devices from unplugged set
@@ -120,79 +131,98 @@ export function updateLeds({
selectedOk.delete(d);
}
const mapping: Array<{
animation: AnimationFunction;
rgbLeds: RGBLed[];
}> = [];
// Handle source slot
if (sourceDrive !== undefined) {
if (unplugged.has(sourceDrive)) {
unplugged.delete(sourceDrive);
// TODO
setLeds(new Set([sourceDrive]), breatheBlue, 2);
} else if (plugged.has(sourceDrive)) {
if (plugged.has(sourceDrive)) {
plugged.delete(sourceDrive);
setLeds(new Set([sourceDrive]), blue);
mapping.push(
setLeds(ledAnimationFunctions.staticBlue, new Set([sourceDrive])),
);
}
}
if (step === 'main') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, white);
setLeds(selectedFailed, white);
mapping.push(
setLeds(
ledAnimationFunctions.staticBlack,
new Set([...unplugged, ...plugged]),
),
setLeds(
ledAnimationFunctions.staticWhite,
new Set([...selectedOk, ...selectedFailed]),
),
);
} else if (step === 'flashing') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, blinkPurple, 2);
setLeds(selectedFailed, red);
mapping.push(
setLeds(
ledAnimationFunctions.staticBlack,
new Set([...unplugged, ...plugged]),
),
setLeds(ledAnimationFunctions.blinkPurple, selectedOk),
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
);
} else if (step === 'verifying') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, blinkGreen, 2);
setLeds(selectedFailed, red);
mapping.push(
setLeds(
ledAnimationFunctions.staticBlack,
new Set([...unplugged, ...plugged]),
),
setLeds(ledAnimationFunctions.blinkGreen, selectedOk),
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
);
} else if (step === 'finish') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, green);
setLeds(selectedFailed, red);
mapping.push(
setLeds(
ledAnimationFunctions.staticBlack,
new Set([...unplugged, ...plugged]),
),
setLeds(ledAnimationFunctions.staticGreen, selectedOk),
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
);
}
}
interface DeviceFromState {
devicePath?: string;
device: string;
animator.mapping = mapping;
}
let ledsState: LedsState | undefined;
function stateObserver(state: typeof DEFAULT_STATE) {
const s = state.toJS();
function stateObserver() {
const s = store.getState().toJS();
let step: 'main' | 'flashing' | 'verifying' | 'finish';
if (s.isFlashing) {
step = s.flashState.type;
} else {
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
}
const availableDrives = s.availableDrives.filter(
(d: DeviceFromState) => d.devicePath,
const availableDrives = getDrives().filter(
(d: DrivelistDrive) => d.devicePath,
);
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
isSourceDrive(d, s.selection.image),
)[0]?.devicePath;
const availableDrivesPaths = availableDrives.map(
(d: DeviceFromState) => d.devicePath,
(d: DrivelistDrive) => d.devicePath,
);
let selectedDrivesPaths: string[];
if (step === 'main') {
selectedDrivesPaths = availableDrives
.filter((d: DrivelistDrive) => s.selection.devices.includes(d.device))
.map((d: DrivelistDrive) => d.devicePath);
selectedDrivesPaths = getSelectedDrives()
.filter((drive) => drive.devicePath !== null)
.map((drive) => drive.devicePath) as string[];
} else {
selectedDrivesPaths = s.devicePaths;
}
const failedDevicePaths = s.failedDeviceErrors.map(
([, { devicePath }]: [string, { devicePath: string }]) => devicePath,
);
const newLedsState = {
step,
sourceDrive: sourceDrivePath,
availableDrives: availableDrivesPaths,
selectedDrives: selectedDrivesPaths,
failedDrives: s.failedDevicePaths,
};
failedDrives: failedDevicePaths,
} as LedsState;
if (!_.isEqual(newLedsState, ledsState)) {
updateLeds(newLedsState);
ledsState = newLedsState;
@@ -215,6 +245,16 @@ export async function init(): Promise<void> {
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
}
ledColors = (await settings.get('ledColors')) || {};
ledAnimationFunctions = {
blinkGreen: createAnimationFunction(blink, ledColors['green']),
blinkPurple: createAnimationFunction(blink, ledColors['purple']),
staticRed: createAnimationFunction(one, ledColors['red']),
staticGreen: createAnimationFunction(one, ledColors['green']),
staticBlue: createAnimationFunction(one, ledColors['blue']),
staticWhite: createAnimationFunction(one, ledColors['white']),
staticBlack: createAnimationFunction(one, ledColors['black']),
};
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
}
}

View File

@@ -1,3 +1,4 @@
import { DrivelistDrive } from '../../../shared/drive-constraints';
/*
* Copyright 2016 balena.io
*
@@ -14,7 +15,7 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import { SourceMetadata } from '../components/source-selector/source-selector';
import * as availableDrives from './available-drives';
import { Actions, store } from './store';
@@ -24,7 +25,7 @@ import { Actions, store } from './store';
*/
export function selectDrive(driveDevice: string) {
store.dispatch({
type: Actions.SELECT_DRIVE,
type: Actions.SELECT_TARGET,
data: driveDevice,
});
}
@@ -40,10 +41,10 @@ export function toggleDrive(driveDevice: string) {
}
}
export function selectImage(image: any) {
export function selectSource(source: SourceMetadata) {
store.dispatch({
type: Actions.SELECT_IMAGE,
data: image,
type: Actions.SELECT_SOURCE,
data: source,
});
}
@@ -57,50 +58,18 @@ export function getSelectedDevices(): string[] {
/**
* @summary Get all selected drive objects
*/
export function getSelectedDrives(): any[] {
const drives = availableDrives.getDrives();
return _.map(getSelectedDevices(), (device) => {
return _.find(drives, { device });
});
export function getSelectedDrives(): DrivelistDrive[] {
const selectedDevices = getSelectedDevices();
return availableDrives
.getDrives()
.filter((drive) => selectedDevices.includes(drive.device));
}
/**
* @summary Get the selected image
*/
export function getImage() {
return _.get(store.getState().toJS(), ['selection', 'image']);
}
export function getImagePath(): string {
return _.get(store.getState().toJS(), ['selection', 'image', 'path']);
}
export function getImageSize(): number {
return _.get(store.getState().toJS(), ['selection', 'image', 'size']);
}
export function getImageUrl(): string {
return _.get(store.getState().toJS(), ['selection', 'image', 'url']);
}
export function getImageName(): string {
return _.get(store.getState().toJS(), ['selection', 'image', 'name']);
}
export function getImageLogo(): string {
return _.get(store.getState().toJS(), ['selection', 'image', 'logo']);
}
export function getImageSupportUrl(): string {
return _.get(store.getState().toJS(), ['selection', 'image', 'supportUrl']);
}
export function getImageRecommendedDriveSize(): number {
return _.get(store.getState().toJS(), [
'selection',
'image',
'recommendedDriveSize',
]);
export function getImage(): SourceMetadata | undefined {
return store.getState().toJS().selection.image;
}
/**
@@ -114,7 +83,7 @@ export function hasDrive(): boolean {
* @summary Check if there is a selected image
*/
export function hasImage(): boolean {
return Boolean(getImage());
return getImage() !== undefined;
}
/**
@@ -122,20 +91,20 @@ export function hasImage(): boolean {
*/
export function deselectDrive(driveDevice: string) {
store.dispatch({
type: Actions.DESELECT_DRIVE,
type: Actions.DESELECT_TARGET,
data: driveDevice,
});
}
export function deselectImage() {
store.dispatch({
type: Actions.DESELECT_IMAGE,
type: Actions.DESELECT_SOURCE,
data: {},
});
}
export function deselectAllDrives() {
_.each(getSelectedDevices(), deselectDrive);
getSelectedDevices().forEach(deselectDrive);
}
/**
@@ -155,5 +124,5 @@ export function isDriveSelected(driveDevice: string) {
}
const selectedDriveDevices = getSelectedDevices();
return _.includes(selectedDriveDevices, driveDevice);
return selectedDriveDevices.includes(driveDevice);
}

View File

@@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings');
const JSON_INDENT = 2;
export const DEFAULT_WIDTH = 800;
export const DEFAULT_HEIGHT = 480;
/**
* @summary Userdata directory path
* @description
@@ -35,12 +38,12 @@ const JSON_INDENT = 2;
* - `~/Library/Application Support/etcher` on macOS
* See https://electronjs.org/docs/api/app#appgetpathname
*
* NOTE: The ternary is due to this module being loaded both,
* Electron's main process and renderer process
* NOTE: We use the remote property when this module
* is loaded in the Electron's renderer process
*/
const USER_DATA_DIR = electron.app
? electron.app.getPath('userData')
: electron.remote.app.getPath('userData');
const app = electron.app || electron.remote.app;
const USER_DATA_DIR = app.getPath('userData');
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
@@ -48,7 +51,7 @@ async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
let contents = '{}';
try {
contents = await fs.readFile(filename, { encoding: 'utf8' });
} catch (error) {
} catch (error: any) {
// noop
}
try {
@@ -73,11 +76,8 @@ export async function writeConfigFile(
}
const DEFAULT_SETTINGS: _.Dictionary<any> = {
unsafeMode: false,
errorReporting: true,
unmountOnSuccess: true,
validateWriteOnSuccess: true,
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
desktopNotifications: true,
autoBlockmapping: true,
decompressFirst: true,
@@ -87,22 +87,24 @@ const settings = _.cloneDeep(DEFAULT_SETTINGS);
async function load(): Promise<void> {
debug('load');
// Use exports.readAll() so it can be mocked in tests
const loadedSettings = await exports.readAll();
const loadedSettings = await readAll();
_.assign(settings, loadedSettings);
}
const loaded = load();
export async function set(key: string, value: any): Promise<void> {
export async function set(
key: string,
value: any,
writeConfigFileFn = writeConfigFile,
): Promise<void> {
debug('set', key, value);
await loaded;
const previousValue = settings[key];
settings[key] = value;
try {
// Use exports.writeConfigFile() so it can be mocked in tests
await exports.writeConfigFile(CONFIG_PATH, settings);
} catch (error) {
await writeConfigFileFn(CONFIG_PATH, settings);
} catch (error: any) {
// Revert to previous value if persisting settings failed
settings[key] = previousValue;
throw error;

View File

@@ -16,6 +16,7 @@
import * as Immutable from 'immutable';
import * as _ from 'lodash';
import { basename } from 'path';
import * as redux from 'redux';
import { v4 as uuidV4 } from 'uuid';
@@ -62,7 +63,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
},
isFlashing: false,
devicePaths: [],
failedDevicePaths: [],
failedDeviceErrors: [],
flashResults: {},
flashState: {
active: 0,
@@ -79,16 +80,16 @@ export const DEFAULT_STATE = Immutable.fromJS({
*/
export enum Actions {
SET_DEVICE_PATHS,
SET_FAILED_DEVICE_PATHS,
SET_AVAILABLE_DRIVES,
SET_FAILED_DEVICE_ERRORS,
SET_AVAILABLE_TARGETS,
SET_FLASH_STATE,
RESET_FLASH_STATE,
SET_FLASHING_FLAG,
UNSET_FLASHING_FLAG,
SELECT_DRIVE,
SELECT_IMAGE,
DESELECT_DRIVE,
DESELECT_IMAGE,
SELECT_TARGET,
SELECT_SOURCE,
DESELECT_TARGET,
DESELECT_SOURCE,
SET_APPLICATION_SESSION_UUID,
SET_FLASHING_WORKFLOW_UUID,
}
@@ -116,7 +117,7 @@ function storeReducer(
action: Action,
): typeof DEFAULT_STATE {
switch (action.type) {
case Actions.SET_AVAILABLE_DRIVES: {
case Actions.SET_AVAILABLE_TARGETS: {
// Type: action.data : Array<DriveObject>
if (!action.data) {
@@ -133,9 +134,16 @@ function storeReducer(
});
}
// Drives order is a list of devicePaths
const drivesOrder = settings.getSync('drivesOrder') ?? [];
drives = _.sortBy(drives, [
// System drives last
(d) => !!d.isSystem,
// Devices with no devicePath first (usbboot)
(d) => !!d.devicePath,
// Sort as defined in the drivesOrder setting if there is one (only for Linux with udev)
(d) => drivesOrder.indexOf(basename(d.devicePath || '')),
// Then sort by devicePath (only available on Linux with udev) or device
(d) => d.devicePath || d.device,
]);
@@ -156,7 +164,7 @@ function storeReducer(
) {
// Deselect this drive gone from availableDrives
return storeReducer(accState, {
type: Actions.DESELECT_DRIVE,
type: Actions.DESELECT_TARGET,
data: device,
});
}
@@ -167,7 +175,7 @@ function storeReducer(
);
const shouldAutoselectAll = Boolean(
settings.getSync('disableExplicitDriveSelection'),
settings.getSync('autoSelectAllDrives'),
);
const AUTOSELECT_DRIVE_COUNT = 1;
const nonStaleSelectedDevices = nonStaleNewState
@@ -189,29 +197,24 @@ function storeReducer(
drives,
(accState, drive) => {
if (
_.every([
constraints.isDriveValid(drive, image),
constraints.isDriveSizeRecommended(drive, image),
// We don't want to auto-select large drives
!constraints.isDriveSizeLarge(drive),
// We don't want to auto-select system drives,
// even when "unsafe mode" is enabled
!constraints.isSystemDrive(drive),
]) ||
(shouldAutoselectAll && constraints.isDriveValid(drive, image))
constraints.isDriveValid(drive, image) &&
!drive.isReadOnly &&
constraints.isDriveSizeRecommended(drive, image) &&
// We don't want to auto-select large drives execpt is autoSelectAllDrives is true
(!constraints.isDriveSizeLarge(drive) || shouldAutoselectAll) &&
// We don't want to auto-select system drives
!constraints.isSystemDrive(drive)
) {
// Auto-select this drive
return storeReducer(accState, {
type: Actions.SELECT_DRIVE,
type: Actions.SELECT_TARGET,
data: drive.device,
});
}
// Deselect this drive in case it still is selected
return storeReducer(accState, {
type: Actions.DESELECT_DRIVE,
type: Actions.DESELECT_TARGET,
data: drive.device,
});
},
@@ -267,7 +270,7 @@ function storeReducer(
.set('flashState', DEFAULT_STATE.get('flashState'))
.set('flashResults', DEFAULT_STATE.get('flashResults'))
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
.set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths'))
.set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
.set(
'lastAverageFlashingSpeed',
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
@@ -293,6 +296,7 @@ function storeReducer(
_.defaults(action.data, {
cancelled: false,
skip: false,
});
if (!_.isBoolean(action.data.cancelled)) {
@@ -333,13 +337,19 @@ function storeReducer(
);
}
if (action.data.skip) {
return state
.set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data));
}
return state
.set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data))
.set('flashState', DEFAULT_STATE.get('flashState'));
}
case Actions.SELECT_DRIVE: {
case Actions.SELECT_TARGET: {
// Type: action.data : String
const device = action.data;
@@ -389,10 +399,12 @@ function storeReducer(
// with image-stream / supported-formats, and have *one*
// place where all the image extension / format handling
// takes place, to avoid having to check 2+ locations with different logic
case Actions.SELECT_IMAGE: {
case Actions.SELECT_SOURCE: {
// Type: action.data : ImageObject
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
if (!action.data.drive) {
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
}
if (!_.isString(action.data.path)) {
throw errors.createError({
@@ -454,7 +466,7 @@ function storeReducer(
!constraints.isDriveSizeRecommended(drive, action.data)
) {
return storeReducer(accState, {
type: Actions.DESELECT_DRIVE,
type: Actions.DESELECT_TARGET,
data: device,
});
}
@@ -465,7 +477,7 @@ function storeReducer(
).setIn(['selection', 'image'], Immutable.fromJS(action.data));
}
case Actions.DESELECT_DRIVE: {
case Actions.DESELECT_TARGET: {
// Type: action.data : String
if (!action.data) {
@@ -489,7 +501,7 @@ function storeReducer(
);
}
case Actions.DESELECT_IMAGE: {
case Actions.DESELECT_SOURCE: {
return state.deleteIn(['selection', 'image']);
}
@@ -505,8 +517,8 @@ function storeReducer(
return state.set('devicePaths', action.data);
}
case Actions.SET_FAILED_DEVICE_PATHS: {
return state.set('failedDevicePaths', action.data);
case Actions.SET_FAILED_DEVICE_ERRORS: {
return state.set('failedDeviceErrors', action.data);
}
default: {

View File

@@ -18,7 +18,7 @@ import * as _ from 'lodash';
import * as resinCorvus from 'resin-corvus/browser';
import * as packageJSON from '../../../../package.json';
import { getConfig, hasProps } from '../../../shared/utils';
import { getConfig } from '../../../shared/utils';
import * as settings from '../models/settings';
import { store } from '../models/store';
@@ -55,7 +55,8 @@ async function initConfig() {
await installCorvus();
let validatedConfig = null;
try {
const config = await getConfig();
const configUrl = await settings.get('configUrl');
const config = await getConfig(configUrl);
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
if (isClientEligible(mixpanelSample)) {
@@ -88,7 +89,7 @@ function validateMixpanelConfig(config: {
const mixpanelConfig = {
api_host: 'https://api.mixpanel.com',
};
if (hasProps(config, ['HTTP_PROTOCOL', 'api_host'])) {
if (config.HTTP_PROTOCOL !== undefined && config.api_host !== undefined) {
mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`;
}
return mixpanelConfig;
@@ -101,10 +102,9 @@ function validateMixpanelConfig(config: {
* This function sends the debug message to product analytics services.
*/
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
const {
applicationSessionUuid,
flashingWorkflowUuid,
} = store.getState().toJS();
const { applicationSessionUuid, flashingWorkflowUuid } = store
.getState()
.toJS();
resinCorvus.logEvent(message, {
...data,
sample: mixpanelSample,

View File

@@ -15,34 +15,31 @@
*/
import * as sdk from 'etcher-sdk';
import {
Adapter,
BlockDeviceAdapter,
UsbbootDeviceAdapter,
} from 'etcher-sdk/build/scanner/adapters';
import { geteuid, platform } from 'process';
import * as settings from '../models/settings';
/**
* @summary returns true if system drives should be shown
*/
function includeSystemDrives() {
return (
settings.getSync('unsafeMode') && !settings.getSync('disableUnsafeMode')
);
}
const adapters: sdk.scanner.adapters.Adapter[] = [
new sdk.scanner.adapters.BlockDeviceAdapter({ includeSystemDrives }),
const adapters: Adapter[] = [
new BlockDeviceAdapter({
includeSystemDrives: () => true,
}),
];
// Can't use permissions.isElevated() here as it returns a promise and we need to set
// module.exports = scanner right now.
if (platform !== 'linux' || geteuid() === 0) {
adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter());
adapters.push(new UsbbootDeviceAdapter());
}
if (
platform === 'win32' &&
sdk.scanner.adapters.DriverlessDeviceAdapter !== undefined
) {
adapters.push(new sdk.scanner.adapters.DriverlessDeviceAdapter());
if (platform === 'win32') {
const {
DriverlessDeviceAdapter: driverless,
// tslint:disable-next-line:no-var-requires
} = require('etcher-sdk/build/scanner/adapters/driverless');
adapters.push(new driverless());
}
export const scanner = new sdk.scanner.Scanner(adapters);

View File

@@ -15,9 +15,8 @@
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as electron from 'electron';
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import { Dictionary } from 'lodash';
import * as ipc from 'node-ipc';
import * as os from 'os';
import * as path from 'path';
@@ -25,7 +24,8 @@ import * as path from 'path';
import * as packageJSON from '../../../../package.json';
import * as errors from '../../../shared/errors';
import * as permissions from '../../../shared/permissions';
import { SourceOptions } from '../components/source-selector/source-selector';
import { getAppPath } from '../../../shared/utils';
import { SourceMetadata } from '../components/source-selector/source-selector';
import * as flashState from '../models/flash-state';
import * as selectionState from '../models/selection-state';
import * as settings from '../models/settings';
@@ -93,7 +93,7 @@ function terminateServer() {
}
function writerArgv(): string[] {
let entryPoint = electron.remote.app.getAppPath();
let entryPoint = path.join(getAppPath(), 'generated', 'child-writer.js');
// AppImages run over FUSE, so the files inside the mount point
// can only be accessed by the user that mounted the AppImage.
// This means we can't re-spawn Etcher as root from the same
@@ -127,29 +127,27 @@ function writerEnv() {
}
interface FlashResults {
skip?: boolean;
cancelled?: boolean;
results?: {
bytesWritten: number;
devices: {
failed: number;
successful: number;
};
errors: Error[];
};
}
/**
* @summary Perform write operation
*
* @description
* This function is extracted for testing purposes.
*/
export async function performWrite(
image: string,
async function performWrite(
image: SourceMetadata,
drives: DrivelistDrive[],
onProgress: sdk.multiWrite.OnProgressFunction,
source: SourceOptions,
): Promise<{ cancelled?: boolean }> {
let cancelled = false;
let skip = false;
ipc.serve();
const {
unmountOnSuccess,
validateWriteOnSuccess,
autoBlockmapping,
decompressFirst,
} = await settings.getAll();
const { autoBlockmapping, decompressFirst } = await settings.getAll();
return await new Promise((resolve, reject) => {
ipc.server.on('error', (error) => {
terminateServer();
@@ -168,22 +166,22 @@ export async function performWrite(
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess,
validateWriteOnSuccess,
};
ipc.server.on('fail', ({ device, error }) => {
if (device.devicePath) {
flashState.addFailedDevicePath(device.devicePath);
flashState.addFailedDeviceError({ device, error });
}
handleErrorLogging(error, analyticsData);
});
ipc.server.on('done', (event) => {
event.results.errors = _.map(event.results.errors, (data) => {
return errors.fromJSON(data);
});
_.merge(flashResults, event);
event.results.errors = event.results.errors.map(
(data: Dictionary<any> & { message: string }) => {
return errors.fromJSON(data);
},
);
flashResults.results = event.results;
});
ipc.server.on('abort', () => {
@@ -191,17 +189,19 @@ export async function performWrite(
cancelled = true;
});
ipc.server.on('skip', () => {
terminateServer();
skip = true;
});
ipc.server.on('state', onProgress);
ipc.server.on('ready', (_data, socket) => {
ipc.server.emit(socket, 'write', {
imagePath: image,
image,
destinations: drives,
source,
SourceType: source.SourceType.name,
validateWriteOnSuccess,
SourceType: image.SourceType.name,
autoBlockmapping,
unmountOnSuccess,
decompressFirst,
});
});
@@ -209,7 +209,7 @@ export async function performWrite(
const argv = writerArgv();
ipc.server.on('start', async () => {
console.log(`Elevating command: ${_.join(argv, ' ')}`);
console.log(`Elevating command: ${argv.join(' ')}`);
const env = writerEnv();
try {
const results = await permissions.elevateCommand(argv, {
@@ -217,7 +217,8 @@ export async function performWrite(
environment: env,
});
flashResults.cancelled = cancelled || results.cancelled;
} catch (error) {
flashResults.skip = skip;
} catch (error: any) {
// This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137;
if (error.code === SIGKILL_EXIT_CODE) {
@@ -230,17 +231,17 @@ export async function performWrite(
}
console.log('Flash results', flashResults);
// This likely means the child died halfway through
// The flash wasn't cancelled and we didn't get a 'done' event
if (
!flashResults.cancelled &&
!_.get(flashResults, ['results', 'bytesWritten'])
!flashResults.skip &&
flashResults.results === undefined
) {
reject(
errors.createUserError({
title: 'The writer process ended unexpectedly',
description:
'Please try again, and contact the Etcher team if the problem persists',
code: 'ECHILDDIED',
}),
);
return;
@@ -258,15 +259,16 @@ export async function performWrite(
* @summary Flash an image to drives
*/
export async function flash(
image: string,
image: SourceMetadata,
drives: DrivelistDrive[],
source: SourceOptions,
// This function is a parameter so it can be mocked in tests
write = performWrite,
): Promise<void> {
if (flashState.isFlashing()) {
throw new Error('There is already a flash in progress');
}
flashState.setFlashingFlag();
await flashState.setFlashingFlag();
flashState.setDevicePaths(
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
);
@@ -278,26 +280,20 @@ export async function flash(
uuid: flashState.getFlashUuid(),
status: 'started',
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
};
analytics.logEvent('Flash', analyticsData);
try {
// Using it from exports so it can be mocked during tests
const result = await exports.performWrite(
image,
drives,
flashState.setProgressState,
source,
);
flashState.unsetFlashingFlag(result);
} catch (error) {
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
const result = await write(image, drives, flashState.setProgressState);
await flashState.unsetFlashingFlag(result);
} catch (error: any) {
await flashState.unsetFlashingFlag({
cancelled: false,
errorCode: error.code,
});
windowProgress.clear();
let { results } = flashState.getFlashResults();
results = results || {};
const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
@@ -316,7 +312,7 @@ export async function flash(
};
analytics.logEvent('Elevation cancelled', eventData);
} else {
const { results } = flashState.getFlashResults();
const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
@@ -332,17 +328,16 @@ export async function flash(
/**
* @summary Cancel write operation
*/
export async function cancel() {
export async function cancel(type: string) {
const status = type.toLowerCase();
const drives = selectionState.getSelectedDevices();
const analyticsData = {
image: selectionState.getImagePath(),
image: selectionState.getImage()?.path,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
status: 'cancel',
status,
};
analytics.logEvent('Cancel', analyticsData);
@@ -352,9 +347,9 @@ export async function cancel() {
// @ts-ignore (no Server.sockets in @types/node-ipc)
const [socket] = ipc.server.sockets;
if (socket !== undefined) {
ipc.server.emit(socket, 'cancel');
ipc.server.emit(socket, status);
}
} catch (error) {
} catch (error: any) {
analytics.logException(error);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { bytesToClosestUnit } from '../../../shared/units';
import * as prettyBytes from 'pretty-bytes';
export interface FlashState {
active: number;
@@ -51,7 +51,7 @@ export function fromFlashState({
} else {
return {
status: 'Flashing...',
position: `${bytesToClosestUnit(position)}`,
position: `${position ? prettyBytes(position) : ''}`,
};
}
} else if (type === 'verifying') {

View File

@@ -18,7 +18,20 @@ import * as electron from 'electron';
import * as _ from 'lodash';
import * as errors from '../../../shared/errors';
import { getAllExtensions } from '../../../shared/supported-formats';
import * as settings from '../../../gui/app/models/settings';
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
async function mountSourceDrive() {
// sourceDrivePath is the name of the link in /dev/disk/by-path
const sourceDrivePath = await settings.get('automountOnFileSelect');
if (sourceDrivePath) {
try {
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
} catch (error: any) {
// noop
}
}
}
/**
* @summary Open an image selection dialog
@@ -27,6 +40,7 @@ import { getAllExtensions } from '../../../shared/supported-formats';
* Notice that by image, we mean *.img/*.iso/*.zip/etc files.
*/
export async function selectImage(): Promise<string | undefined> {
await mountSourceDrive();
const options: electron.OpenDialogOptions = {
// This variable is set when running in GNU/Linux from
// inside an AppImage, and represents the working directory
@@ -40,7 +54,11 @@ export async function selectImage(): Promise<string | undefined> {
filters: [
{
name: 'OS Images',
extensions: [...getAllExtensions()].sort(),
extensions: SUPPORTED_EXTENSIONS,
},
{
name: 'All',
extensions: ['*'],
},
],
};

View File

@@ -50,9 +50,9 @@ export const currentWindow = electron.remote.getCurrentWindow();
*/
export function set(state: FlashState) {
if (state.percentage != null) {
exports.currentWindow.setProgressBar(percentageToFloat(state.percentage));
currentWindow.setProgressBar(percentageToFloat(state.percentage));
}
exports.currentWindow.setTitle(getWindowTitle(state));
currentWindow.setTitle(getWindowTitle(state));
}
/**
@@ -60,6 +60,6 @@ export function set(state: FlashState) {
*/
export function clear() {
// Passing 0 or null/undefined doesn't work.
exports.currentWindow.setProgressBar(-1);
exports.currentWindow.setTitle(getWindowTitle(undefined));
currentWindow.setProgressBar(-1);
currentWindow.setTitle(getWindowTitle(undefined));
}

View File

@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { using } from 'bluebird';
import { exec } from 'child_process';
import { withTmpFile } from 'etcher-sdk/build/tmp';
import { readFile } from 'fs';
import { chain, trim } from 'lodash';
import { platform } from 'os';
@@ -23,8 +23,6 @@ import { join } from 'path';
import { env } from 'process';
import { promisify } from 'util';
import { tmpFileDisposer } from '../../../shared/utils';
const readFileAsync = promisify(readFile);
const execAsync = promisify(exec);
@@ -32,8 +30,7 @@ const execAsync = promisify(exec);
/**
* @summary Returns wmic's output for network drives
*/
export async function getWmicNetworkDrivesOutput(): Promise<string> {
// Exported for tests.
async function getWmicNetworkDrivesOutput(): Promise<string> {
// When trying to read wmic's stdout directly from node, it is encoded with the current
// console codepage (depending on the computer).
// Decoding this would require getting this codepage somehow and using iconv as node
@@ -43,11 +40,11 @@ export async function getWmicNetworkDrivesOutput(): Promise<string> {
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
const options = {
// Close the file once it's created
discardDescriptor: true,
keepOpen: false,
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
prefix: 'tmp',
};
return using(tmpFileDisposer(options), async ({ path }) => {
return withTmpFile(options, async ({ path }) => {
const command = [
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
'path',
@@ -67,9 +64,10 @@ export async function getWmicNetworkDrivesOutput(): Promise<string> {
/**
* @summary returns a Map of drive letter -> network locations on Windows: 'Z:' -> '\\\\192.168.0.1\\Public'
*/
async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
// Use getWindowsNetworkDrives from "exports." so it can be mocked in tests
const result = await exports.getWmicNetworkDrivesOutput();
async function getWindowsNetworkDrives(
getWmicOutput: () => Promise<string>,
): Promise<Map<string, string>> {
const result = await getWmicOutput();
const couples: Array<[string, string]> = chain(result)
.split('\n')
// Remove header line
@@ -98,13 +96,15 @@ async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
*/
export async function replaceWindowsNetworkDriveLetter(
filePath: string,
// getWmicOutput is a parameter so it can be replaced in tests
getWmicOutput = getWmicNetworkDrivesOutput,
): Promise<string> {
let result = filePath;
if (platform() === 'win32') {
const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath);
if (matches !== null) {
const [, drive, relativePath] = matches;
const drives = await getWindowsNetworkDrives();
const drives = await getWindowsNetworkDrives(getWmicOutput);
const location = drives.get(drive);
if (location !== undefined) {
result = `${location}\\${relativePath}`;

View File

@@ -1,124 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
.page-finish {
margin-top: 60px;
}
.page-finish .title,
.page-finish .title h3 {
color: $palette-theme-dark-foreground;
font-weight: bold;
}
.page-finish .center {
display: flex;
align-items: center;
justify-content: center;
}
.page-finish .box > div > button {
margin-right: 20px;
}
.page-finish webview {
width: 800px;
height: 300px;
position: absolute;
top: 80px;
left: 0;
z-index: 9001;
}
.page-finish .fallback-banner {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: absolute;
bottom: 0;
color: white;
height: 320px;
width: 100vw;
left: 0;
> * {
display: flex;
justify-content: center;
align-items: center;
}
.caption {
display: flex;
font-weight: 500;
}
.caption-big {
font-size: 28px;
font-weight: bold;
position: absolute;
top: 75px;
}
.caption-small {
font-size: 12px;
}
.fallback-footer {
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
position: absolute;
bottom: 0;
max-height: 21px;
margin-bottom: 17px;
}
.svg-icon {
margin: 0 10px;
}
.section-footer {
position: absolute;
right: 0;
bottom: 0;
.footer-right {
color: #7e8085;
font-size: 12px;
margin-right: 30px;
}
}
}
.inline-flex {
display: inline-flex;
}
.page-finish .tick {
/* hack(Shou): for some reason the height is stretched */
height: 24px;
width: 24px;
border: none;
padding: 0;
margin: 0 15px 0 0;
justify-content: center;
align-items: center;
display: flex;
font-size: 16px;
}

View File

@@ -1,136 +0,0 @@
/*
* Copyright 2016 balena.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 _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { TargetSelector } from '../../components/drive-selector/target-selector';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import * as settings from '../../models/settings';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
const StepBorder = styled.div<{
disabled: boolean;
left?: boolean;
right?: boolean;
}>`
height: 2px;
background-color: ${(props) =>
props.disabled
? props.theme.colors.dark.disabled.foreground
: props.theme.colors.dark.foreground};
position: absolute;
width: 124px;
top: 19px;
left: ${(props) => (props.left ? '-67px' : undefined)};
right: ${(props) => (props.right ? '-67px' : undefined)};
`;
const getDriveListLabel = () => {
return _.join(
_.map(getSelectedDrives(), (drive: any) => {
return `${drive.description} (${drive.displayName})`;
}),
'\n',
);
};
const shouldShowDrivesButton = () => {
return !settings.getSync('disableExplicitDriveSelection');
};
const getDriveSelectionStateSlice = () => ({
showDrivesButton: shouldShowDrivesButton(),
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
});
interface DriveSelectorProps {
webviewShowing: boolean;
disabled: boolean;
nextStepDisabled: boolean;
hasDrive: boolean;
flashing: boolean;
}
export const DriveSelector = ({
webviewShowing,
disabled,
nextStepDisabled,
hasDrive,
flashing,
}: DriveSelectorProps) => {
// TODO: inject these from redux-connector
const [
{ showDrivesButton, driveListLabel, targets, image },
setStateSlice,
] = React.useState(getDriveSelectionStateSlice());
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
false,
);
React.useEffect(() => {
return observe(() => {
setStateSlice(getDriveSelectionStateSlice());
});
}, []);
const showStepConnectingLines = !webviewShowing || !flashing;
return (
<div className="box text-center relative">
{showStepConnectingLines && (
<>
<StepBorder disabled={disabled} left />
<StepBorder disabled={nextStepDisabled} right />
</>
)}
<div className="center-block">
<SVGIcon paths={['drive.svg']} disabled={disabled} />
</div>
<div className="space-vertical-large">
<TargetSelector
disabled={disabled}
show={!hasDrive && showDrivesButton}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowDriveSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowDriveSelectorModal(true);
}}
flashing={flashing}
targets={targets}
image={image}
/>
</div>
{showDriveSelectorModal && (
<DriveSelectorModal
close={() => setShowDriveSelectorModal(false)}
></DriveSelectorModal>
)}
</div>
);
};

View File

@@ -14,17 +14,15 @@
* limitations under the License.
*/
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import * as _ from 'lodash';
import * as path from 'path';
import * as React from 'react';
import { Flex, Modal, Txt } from 'rendition';
import { Flex, Modal as SmallModal, Txt } from 'rendition';
import * as constraints from '../../../../shared/drive-constraints';
import * as messages from '../../../../shared/messages';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { ProgressButton } from '../../components/progress-button/progress-button';
import { SourceOptions } from '../../components/source-selector/source-selector';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import * as availableDrives from '../../models/available-drives';
import * as flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state';
@@ -32,27 +30,17 @@ import * as analytics from '../../modules/analytics';
import { scanner as driveScanner } from '../../modules/drive-scanner';
import * as imageWriter from '../../modules/image-writer';
import * as notification from '../../os/notification';
import {
selectAllTargets,
TargetSelectorModal,
} from '../../components/target-selector/target-selector';
import FlashSvg from '../../../assets/flash.svg';
import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal';
const COMPLETED_PERCENTAGE = 100;
const SPEED_PRECISION = 2;
const getWarningMessages = (drives: any, image: any) => {
const warningMessages = [];
for (const drive of drives) {
if (constraints.isDriveSizeLarge(drive)) {
warningMessages.push(messages.warning.largeDriveSize(drive));
} else if (!constraints.isDriveSizeRecommended(drive, image)) {
warningMessages.push(
messages.warning.unrecommendedDriveSize(image, drive),
);
}
// TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode
}
return warningMessages;
};
const getErrorMessageFromCode = (errorCode: string) => {
// TODO: All these error codes to messages translations
// should go away if the writer emitted user friendly
@@ -71,15 +59,35 @@ const getErrorMessageFromCode = (errorCode: string) => {
return '';
};
function notifySuccess(
iconPath: string,
basename: string,
drives: any,
devices: { successful: number; failed: number },
) {
notification.send(
'Flash complete!',
messages.info.flashComplete(basename, drives, devices),
iconPath,
);
}
function notifyFailure(iconPath: string, basename: string, drives: any) {
notification.send(
'Oops! Looks like the flash failed.',
messages.error.flashFailure(basename, drives),
iconPath,
);
}
async function flashImageToDrive(
isFlashing: boolean,
goToSuccess: () => void,
sourceOptions: SourceOptions,
): Promise<string> {
const devices = selection.getSelectedDevices();
const image: any = selection.getImage();
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
return _.includes(devices, drive.device);
const drives = availableDrives.getDrives().filter((drive: any) => {
return devices.includes(drive.device);
});
if (drives.length === 0 || isFlashing) {
@@ -93,26 +101,24 @@ async function flashImageToDrive(
const iconPath = path.join('media', 'icon.png');
const basename = path.basename(image.path);
try {
await imageWriter.flash(image.path, drives, sourceOptions);
await imageWriter.flash(image, drives);
if (!flashState.wasLastFlashCancelled()) {
const flashResults: any = flashState.getFlashResults();
notification.send(
'Flash complete!',
messages.info.flashComplete(
basename,
drives as any,
flashResults.results.devices,
),
iconPath,
);
const {
results = { devices: { successful: 0, failed: 0 } },
skip,
cancelled,
} = flashState.getFlashResults();
if (!skip && !cancelled) {
if (results.devices.successful > 0) {
notifySuccess(iconPath, basename, drives, results.devices);
} else {
notifyFailure(iconPath, basename, drives);
}
}
goToSuccess();
}
} catch (error) {
notification.send(
'Oops! Looks like the flash failed.',
messages.error.flashFailure(path.basename(image.path), drives),
iconPath,
);
} catch (error: any) {
notifyFailure(iconPath, basename, drives);
let errorMessage = getErrorMessageFromCode(error.code);
if (!errorMessage) {
error.image = basename;
@@ -129,7 +135,7 @@ async function flashImageToDrive(
}
const formatSeconds = (totalSeconds: number) => {
if (!totalSeconds && !_.isNumber(totalSeconds)) {
if (typeof totalSeconds !== 'number' || !Number.isFinite(totalSeconds)) {
return '';
}
const minutes = Math.floor(totalSeconds / 60);
@@ -141,8 +147,8 @@ const formatSeconds = (totalSeconds: number) => {
interface FlashStepProps {
shouldFlashStepBeDisabled: boolean;
goToSuccess: () => void;
source: SourceOptions;
isFlashing: boolean;
style?: React.CSSProperties;
// TODO: factorize
step: 'decompressing' | 'flashing' | 'verifying';
percentage: number;
@@ -150,12 +156,19 @@ interface FlashStepProps {
failed: number;
speed?: number;
eta?: number;
width: string;
}
export interface DriveWithWarnings extends constraints.DrivelistDrive {
statuses: constraints.DriveStatus[];
}
interface FlashStepState {
warningMessages: string[];
warningMessage: boolean;
errorMessage: string;
showDriveSelectorModal: boolean;
systemDrives: boolean;
drivesWithWarnings: DriveWithWarnings[];
}
export class FlashStep extends React.PureComponent<
@@ -165,14 +178,16 @@ export class FlashStep extends React.PureComponent<
constructor(props: FlashStepProps) {
super(props);
this.state = {
warningMessages: [],
warningMessage: false,
errorMessage: '',
showDriveSelectorModal: false,
systemDrives: false,
drivesWithWarnings: [],
};
}
private async handleWarningResponse(shouldContinue: boolean) {
this.setState({ warningMessages: [] });
this.setState({ warningMessage: false });
if (!shouldContinue) {
this.setState({ showDriveSelectorModal: true });
return;
@@ -181,7 +196,6 @@ export class FlashStep extends React.PureComponent<
errorMessage: await flashImageToDrive(
this.props.isFlashing,
this.props.goToSuccess,
this.props.source,
),
});
}
@@ -196,31 +210,49 @@ export class FlashStep extends React.PureComponent<
}
}
private hasListWarnings(drives: any[]) {
if (drives.length === 0 || flashState.isFlashing()) {
return;
}
return drives.filter((drive) => drive.isSystem).length > 0;
}
private async tryFlash() {
const devices = selection.getSelectedDevices();
const image = selection.getImage();
const drives = _.filter(
availableDrives.getDrives(),
(drive: { device: string }) => {
return _.includes(devices, drive.device);
},
);
const drives = selection.getSelectedDrives().map((drive) => {
return {
...drive,
statuses: constraints.getDriveImageCompatibilityStatuses(
drive,
undefined,
true,
),
};
});
if (drives.length === 0 || this.props.isFlashing) {
return;
}
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(
drives,
image,
);
const hasDangerStatus = drives.some((drive) => drive.statuses.length > 0);
if (hasDangerStatus) {
this.setState({ warningMessages: getWarningMessages(drives, image) });
const systemDrives = drives.some((drive) =>
drive.statuses.includes(constraints.statuses.system),
);
this.setState({
systemDrives,
drivesWithWarnings: drives.filter((driveWithWarnings) => {
return (
driveWithWarnings.isSystem ||
(!systemDrives &&
driveWithWarnings.statuses.includes(constraints.statuses.large))
);
}),
warningMessage: true,
});
return;
}
this.setState({
errorMessage: await flashImageToDrive(
this.props.isFlashing,
this.props.goToSuccess,
this.props.source,
),
});
}
@@ -228,81 +260,66 @@ export class FlashStep extends React.PureComponent<
public render() {
return (
<>
<div className="box text-center">
<div className="center-block">
<SVGIcon
paths={['flash.svg']}
disabled={this.props.shouldFlashStepBeDisabled}
/>
</div>
<div className="space-vertical-large">
<ProgressButton
type={this.props.step}
active={this.props.isFlashing}
percentage={this.props.percentage}
position={this.props.position}
disabled={this.props.shouldFlashStepBeDisabled}
cancel={imageWriter.cancel}
callback={() => {
this.tryFlash();
}}
/>
{!_.isNil(this.props.speed) &&
this.props.percentage !== COMPLETED_PERCENTAGE && (
<Flex
justifyContent="space-between"
fontSize="14px"
color="#7e8085"
>
{!_.isNil(this.props.speed) && (
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
)}
{!_.isNil(this.props.eta) && (
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
)}
</Flex>
)}
{Boolean(this.props.failed) && (
<div className="target-status-wrap">
<div className="target-status-line target-status-failed">
<span className="target-status-dot"></span>
<span className="target-status-quantity">
{this.props.failed}
</span>
<span className="target-status-message">
{messages.progress.failed(this.props.failed)}{' '}
</span>
</div>
</div>
)}
</div>
</div>
{this.state.warningMessages.length > 0 && (
<Modal
width={400}
titleElement={'Attention'}
cancel={() => this.handleWarningResponse(false)}
done={() => this.handleWarningResponse(true)}
cancelButtonProps={{
children: 'Change',
<Flex
flexDirection="column"
alignItems="start"
width={this.props.width}
style={this.props.style}
>
<FlashSvg
width="40px"
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
style={{
margin: '0 auto',
}}
action={'Continue'}
primaryButtonProps={{ primary: false, warning: true }}
>
{_.map(this.state.warningMessages, (message, key) => (
<Txt key={key} whitespace="pre-line" mt={2}>
{message}
</Txt>
))}
</Modal>
/>
<ProgressButton
type={this.props.step}
active={this.props.isFlashing}
percentage={this.props.percentage}
position={this.props.position}
disabled={this.props.shouldFlashStepBeDisabled}
cancel={imageWriter.cancel}
warning={this.hasListWarnings(selection.getSelectedDrives())}
callback={() => this.tryFlash()}
/>
{!_.isNil(this.props.speed) &&
this.props.percentage !== COMPLETED_PERCENTAGE && (
<Flex
justifyContent="space-between"
fontSize="14px"
color="#7e8085"
width="100%"
>
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
{!_.isNil(this.props.eta) && (
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
)}
</Flex>
)}
{Boolean(this.props.failed) && (
<Flex color="#fff" alignItems="center" mt={35}>
<CircleSvg height="1em" fill="#ff4444" />
<Txt ml={10}>{this.props.failed}</Txt>
<Txt ml={10}>{messages.progress.failed(this.props.failed)}</Txt>
</Flex>
)}
</Flex>
{this.state.warningMessage && (
<DriveStatusWarningModal
done={() => this.handleWarningResponse(true)}
cancel={() => this.handleWarningResponse(false)}
isSystem={this.state.systemDrives}
drivesWithWarnings={this.state.drivesWithWarnings}
/>
)}
{this.state.errorMessage && (
<Modal
<SmallModal
width={400}
titleElement={'Attention'}
cancel={() => this.handleFlashErrorResponse(false)}
@@ -310,16 +327,20 @@ export class FlashStep extends React.PureComponent<
action={'Retry'}
>
<Txt>
{_.map(this.state.errorMessage.split('\n'), (message, key) => (
{this.state.errorMessage.split('\n').map((message, key) => (
<p key={key}>{message}</p>
))}
</Txt>
</Modal>
</SmallModal>
)}
{this.state.showDriveSelectorModal && (
<DriveSelectorModal
close={() => this.setState({ showDriveSelectorModal: false })}
<TargetSelectorModal
write={true}
cancel={() => this.setState({ showDriveSelectorModal: false })}
done={(modalTargets) => {
selectAllTargets(modalTargets);
this.setState({ showDriveSelectorModal: false });
}}
/>
)}
</>

View File

@@ -14,25 +14,22 @@
* limitations under the License.
*/
import { faCog, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { sourceDestination } from 'etcher-sdk';
import * as _ from 'lodash';
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
import * as path from 'path';
import * as prettyBytes from 'pretty-bytes';
import * as React from 'react';
import { Flex } from 'rendition';
import styled from 'styled-components';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { FeaturedProject } from '../../components/featured-project/featured-project';
import FinishPage from '../../components/finish/finish';
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
import { SettingsModal } from '../../components/settings/settings';
import {
SourceOptions,
SourceMetadata,
SourceSelector,
} from '../../components/source-selector/source-selector';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings';
@@ -42,13 +39,16 @@ import {
IconButton as BaseIcon,
ThemedProvider,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { bytesToClosestUnit } from '../../../../shared/units';
import { DriveSelector } from './DriveSelector';
import {
TargetSelector,
getDriveListLabel,
} from '../../components/target-selector/target-selector';
import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
const Icon = styled(BaseIcon)`
margin-right: 20px;
`;
@@ -67,31 +67,54 @@ function getDrivesTitle() {
return `${drives.length} Targets`;
}
function getImageBasename() {
if (!selectionState.hasImage()) {
function getImageBasename(image?: SourceMetadata) {
if (image === undefined) {
return '';
}
const selectionImageName = selectionState.getImageName();
const imageBasename = path.basename(selectionState.getImagePath());
return selectionImageName || imageBasename;
if (image.drive) {
return image.drive.description;
}
const imageBasename = path.basename(image.path);
return image.name || imageBasename;
}
const StepBorder = styled.div<{
disabled: boolean;
left?: boolean;
right?: boolean;
}>`
position: relative;
height: 2px;
background-color: ${(props) =>
props.disabled
? props.theme.colors.dark.disabled.foreground
: props.theme.colors.dark.foreground};
width: 120px;
top: 19px;
left: ${(props) => (props.left ? '-67px' : undefined)};
margin-right: ${(props) => (props.left ? '-120px' : undefined)};
right: ${(props) => (props.right ? '-67px' : undefined)};
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
`;
interface MainPageStateFromStore {
isFlashing: boolean;
hasImage: boolean;
hasDrive: boolean;
imageLogo: string;
imageSize: number;
imageName: string;
imageLogo?: string;
imageSize?: number;
imageName?: string;
driveTitle: string;
driveLabel: string;
}
interface MainPageState {
current: 'main' | 'success';
isWebviewShowing: boolean;
hideSettings: boolean;
source: SourceOptions;
featuredProjectURL?: string;
}
export class MainPage extends React.Component<
@@ -104,30 +127,39 @@ export class MainPage extends React.Component<
current: 'main',
isWebviewShowing: false,
hideSettings: true,
source: {
imagePath: '',
SourceType: sourceDestination.File,
},
...this.stateHelper(),
};
}
private stateHelper(): MainPageStateFromStore {
const image = selectionState.getImage();
return {
isFlashing: flashState.isFlashing(),
hasImage: selectionState.hasImage(),
hasDrive: selectionState.hasDrive(),
imageLogo: selectionState.getImageLogo(),
imageSize: selectionState.getImageSize(),
imageName: getImageBasename(),
imageLogo: image?.logo,
imageSize: image?.size,
imageName: getImageBasename(selectionState.getImage()),
driveTitle: getDrivesTitle(),
driveLabel: getDriveListLabel(),
};
}
public componentDidMount() {
private async getFeaturedProjectURL() {
const url = new URL(
(await settings.get('featuredProjectEndpoint')) ||
'https://assets.balena.io/etcher-featured/index.html',
);
url.searchParams.append('borderRight', 'false');
url.searchParams.append('darkBackground', 'true');
return url.toString();
}
public async componentDidMount() {
observe(() => {
this.setState(this.stateHelper());
});
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
}
private renderMain() {
@@ -135,55 +167,164 @@ export class MainPage extends React.Component<
const shouldDriveStepBeDisabled = !this.state.hasImage;
const shouldFlashStepBeDisabled =
!this.state.hasImage || !this.state.hasDrive;
const notFlashingOrSplitView =
!this.state.isFlashing || !this.state.isWebviewShowing;
return (
<>
<header
id="app-header"
style={{
width: '100%',
padding: '13px 14px',
textAlign: 'center',
}}
>
<span
style={{
cursor: 'pointer',
}}
onClick={() =>
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
}
tabIndex={100}
>
<SVGIcon paths={['etcher.svg']} width="123px" height="22px" />
</span>
<Flex
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
justifyContent="space-between"
>
{notFlashingOrSplitView && (
<>
<SourceSelector flashing={this.state.isFlashing} />
<Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex>
<TargetSelector
disabled={shouldDriveStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
</>
)}
<span
{this.state.isFlashing && this.state.isWebviewShowing && (
<Flex
style={{
position: 'absolute',
top: 0,
left: 0,
width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
}}
/>
</Flex>
)}
{this.state.isFlashing && this.state.featuredProjectURL && (
<SafeWebview
src={this.state.featuredProjectURL}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{
float: 'right',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
>
/>
)}
<FlashStep
width={this.state.isWebviewShowing ? '220px' : '200px'}
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
style={{ zIndex: 1 }}
/>
</Flex>
);
}
private renderSuccess() {
return (
<FinishPage
goToMain={() => {
flashState.resetState();
this.setState({ current: 'main' });
}}
/>
);
}
public render() {
return (
<ThemedProvider style={{ height: '100%', width: '100%' }}>
<Flex
justifyContent="space-between"
alignItems="center"
paddingTop="14px"
style={{
// Allow window to be dragged from header
// @ts-ignore
WebkitAppRegion: 'drag',
position: 'relative',
zIndex: 2,
}}
>
<Flex width="100%" />
<Flex width="100%" alignItems="center" justifyContent="center">
<EtcherSvg
width="123px"
height="22px"
style={{
cursor: 'pointer',
}}
onClick={() =>
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
}
tabIndex={100}
/>
</Flex>
<Flex width="100%" alignItems="center" justifyContent="flex-end">
<Icon
icon={<FontAwesomeIcon icon={faCog} />}
icon={<CogSvg height="1em" fill="currentColor" />}
plain
tabIndex={5}
onClick={() => this.setState({ hideSettings: false })}
style={{
// Make touch events click instead of dragging
WebkitAppRegion: 'no-drag',
}}
/>
{!settings.getSync('disableExternalLinks') && (
<Icon
icon={<FontAwesomeIcon icon={faQuestionCircle} />}
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
onClick={() =>
openExternal(
selectionState.getImageSupportUrl() ||
selectionState.getImage()?.supportUrl ||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
)
}
tabIndex={6}
style={{
// Make touch events click instead of dragging
WebkitAppRegion: 'no-drag',
}}
/>
)}
</span>
</header>
</Flex>
</Flex>
{this.state.hideSettings ? null : (
<SettingsModal
toggleModal={(value: boolean) => {
@@ -191,96 +332,6 @@ export class MainPage extends React.Component<
}}
/>
)}
<Flex
className="page-main row around-xs"
style={{ margin: '110px 50px' }}
>
<div className="col-xs">
<SourceSelector
flashing={this.state.isFlashing}
afterSelected={(source: SourceOptions) =>
this.setState({ source })
}
/>
</div>
<div className="col-xs">
<DriveSelector
webviewShowing={this.state.isWebviewShowing}
disabled={shouldDriveStepBeDisabled}
nextStepDisabled={shouldFlashStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
</div>
{this.state.isFlashing && (
<div
className={`featured-project ${
this.state.isFlashing && this.state.isWebviewShowing
? 'fp-visible'
: ''
}`}
>
<FeaturedProject
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
/>
</div>
)}
<div>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={middleEllipsis(this.state.imageName, 16)}
imageSize={
_.isNumber(this.state.imageSize)
? (bytesToClosestUnit(this.state.imageSize) as string)
: ''
}
driveTitle={middleEllipsis(this.state.driveTitle, 16)}
shouldShow={this.state.isFlashing && this.state.isWebviewShowing}
/>
</div>
<div className="col-xs">
<FlashStep
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
source={this.state.source}
isFlashing={flashState.isFlashing()}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
/>
</div>
</Flex>
</>
);
}
private renderSuccess() {
return (
<div className="section-loader isFinish">
<FinishPage
goToMain={() => {
flashState.resetState();
this.setState({ current: 'main' });
}}
/>
<SafeWebview src="https://www.balena.io/etcher/success-banner/" />
</div>
);
}
public render() {
return (
<ThemedProvider style={{ height: '100%', width: '100%' }}>
{this.state.current === 'main'
? this.renderMain()
: this.renderSuccess()}

View File

@@ -1,89 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
img[disabled] {
opacity: $disabled-opacity;
}
.page-main {
flex: 1;
align-self: center;
margin: 20px;
}
.page-main > .col-xs {
height: 165px;
}
.page-main .relative {
position: relative;
}
.page-main .glyphicon {
vertical-align: text-top;
}
.page-main .step-name {
display: flex;
justify-content: center;
align-items: center;
height: 39px;
width: 100%;
font-weight: bold;
color: $palette-theme-primary-foreground;
}
.target-status-wrap {
display: flex;
position: absolute;
top: 62px;
flex-direction: column;
margin: 8px 28px;
align-items: flex-start;
}
.target-status-line {
display: flex;
align-items: baseline;
> .target-status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
}
&.target-status-successful > .target-status-dot {
background-color: $palette-theme-success-background;
}
&.target-status-failed > .target-status-dot {
background-color: $palette-theme-danger-background;
}
> .target-status-quantity {
color: white;
font-weight: bold;
}
> .target-status-message {
color: gray;
margin-left: 10px;
}
}
.space-vertical-large {
position: relative;
}

10
lib/gui/app/renderer.ts Normal file
View File

@@ -0,0 +1,10 @@
// @ts-nocheck
import { main } from './app';
if (module.hot) {
module.hot.accept('./app', () => {
main();
});
}
main();

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
.label {
font-size: 9px;
margin-right: 4.5px;
}
.label-big {
font-size: 11px;
padding: 8px 25px;
}
.label-inset {
background-color: darken($palette-theme-dark-background, 10%);
color: darken($palette-theme-dark-foreground, 43%);
}
.label-danger {
background-color: $palette-theme-danger-background;
color: $palette-theme-danger-foreground;
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
.tick {
@extend .glyphicon;
display: inline-block;
border-radius: 50%;
padding: 3px;
font-size: 18px;
border: 2px solid;
&[disabled] {
color: $palette-theme-dark-soft-foreground;
border-color: $palette-theme-dark-soft-foreground;
background-color: transparent;
}
}
.tick--success {
@extend .glyphicon-ok;
color: $palette-theme-success-foreground;
background-color: $palette-theme-success-background;
border-color: $palette-theme-success-background;
}
.tick--error {
@extend .glyphicon-remove;
color: $palette-theme-danger-foreground;
background-color: $palette-theme-danger-background;
border-color: $palette-theme-danger-background;
}

View File

@@ -1,107 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
$icon-font-path: "../../../../node_modules/bootstrap-sass/assets/fonts/bootstrap/";
$font-size-base: 16px;
$cursor-disabled: initial;
$link-hover-decoration: none;
$btn-min-width: 170px;
$link-color: #ddd;
$disabled-opacity: 0.2;
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
@import "./modules/theme";
@import "./modules/bootstrap";
@import "./modules/space";
@import "./components/label";
@import "./components/tick";
@import "../components/drive-selector/styles/drive-selector";
@import "../pages/main/styles/main";
@import "../pages/finish/styles/finish";
@import "./desktop";
@font-face {
font-family: "SourceSansPro";
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "SourceSansPro";
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-display: block;
}
body {
letter-spacing: 0.1px;
display: flex;
flex-direction: column;
font-family: "SourceSansPro";
> header {
flex: 0 0 auto;
}
> main {
flex: 1;
display: flex;
}
> footer {
flex: 0 0 auto;
}
}
.section-loader {
webview {
flex: 0 1;
height: 0;
width: 0;
}
&.isFinish webview {
flex: initial;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 320px;
}
}
.featured-project {
webview {
flex: 0 1;
height: 0;
width: 0;
}
&.fp-visible webview {
width: 480px;
height: 360px;
position: absolute;
z-index: 1;
left: 30px;
top: 45px;
border-radius: 7px;
overflow: hidden;
}
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
// This file is meant to hold Bootstrap modifications
// that don't qualify as separate UI components.
// Prevent white flash when running application
html {
background-color: $palette-theme-dark-background;
}
body {
background-color: $palette-theme-dark-background;
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
$spacing-large: 30px;
$spacing-medium: 15px;
$spacing-small: 10px;
$spacing-tiny: 5px;
.space-medium {
margin: $spacing-medium;
}
.space-vertical-medium {
margin-top: $spacing-medium;
margin-bottom: $spacing-medium;
}
.space-vertical-small {
margin-top: $spacing-small;
margin-bottom: $spacing-small;
}
.space-top-large {
margin-top: $spacing-large;
}
.space-vertical-large {
margin-top: $spacing-large;
margin-bottom: $spacing-large;
}
.space-bottom-medium {
margin-bottom: $spacing-medium;
}
.space-bottom-large {
margin-bottom: $spacing-large;
}
.space-right-tiny {
margin-right: $spacing-tiny;
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
$palette-theme-dark-foreground: #fff;
$palette-theme-dark-background: #4d5057;
$palette-theme-light-foreground: #666;
$palette-theme-light-background: #fff;
$palette-theme-dark-soft-foreground: #ddd;
$palette-theme-dark-soft-background: #64686a;
$palette-theme-light-soft-foreground: #b3b3b3;
$palette-theme-dark-disabled-background: #3a3c41;
$palette-theme-dark-disabled-foreground: #787c7f;
$palette-theme-light-disabled-background: #d5d5d5;
$palette-theme-light-disabled-foreground: #787c7f;
$palette-theme-default-background: #ececec;
$palette-theme-default-foreground: #b3b3b3;
$palette-theme-primary-background: #2297de;
$palette-theme-primary-foreground: #fff;
$palette-theme-warning-background: #ff912f;
$palette-theme-warning-foreground: #fff;
$palette-theme-danger-background: #d9534f;
$palette-theme-danger-foreground: #fff;
$palette-theme-success-background: #5fb835;
$palette-theme-success-foreground: #fff;

View File

@@ -14,57 +14,32 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import * as React from 'react';
import { Button, ButtonProps, Provider, Txt } from 'rendition';
import styled from 'styled-components';
import { space } from 'styled-system';
import {
Alert as AlertBase,
Flex,
FlexProps,
Button,
ButtonProps,
Modal as ModalBase,
Provider,
Table as BaseTable,
TableProps as BaseTableProps,
Txt,
} from 'rendition';
import styled, { css } from 'styled-components';
import { colors } from './theme';
const font = 'SourceSansPro';
const theme = {
font,
titleFont: font,
global: {
font: {
family: font,
},
},
colors,
button: {
border: {
width: '0',
radius: '24px',
},
disabled: {
opacity: 1,
},
extend: () => `
&& {
width: 200px;
height: 48px;
&: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};
}
}
}
`,
},
};
import { colors, theme } from './theme';
export const ThemedProvider = (props: any) => (
<Provider theme={theme} {...props}></Provider>
);
export const BaseButton = styled(Button)`
width: 200px;
height: 48px;
font-size: 16px;
`;
export const IconButton = styled((props) => <Button plain {...props} />)`
@@ -81,10 +56,10 @@ export const IconButton = styled((props) => <Button plain {...props} />)`
`;
export const StepButton = styled((props: ButtonProps) => (
<Button {...props}></Button>
<BaseButton {...props}></BaseButton>
))`
color: #ffffff;
margin: auto;
font-size: 14px;
`;
export const ChangeButton = styled(Button)`
@@ -102,17 +77,15 @@ export const ChangeButton = styled(Button)`
color: #8f9297;
}
}
${space}
}
`;
export const StepNameButton = styled(Button)`
border-radius: 24px;
margin: auto;
display: flex;
export const StepNameButton = styled(BaseButton)`
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
font-weight: bold;
font-weight: normal;
color: ${colors.dark.foreground};
&:enabled {
@@ -123,16 +96,230 @@ export const StepNameButton = styled(Button)`
}
}
`;
export const Footer = styled(Txt)`
margin-top: 10px;
color: ${colors.dark.disabled.foreground};
font-size: 10px;
`;
export const Underline = styled(Txt.span)`
border-bottom: 1px dotted;
padding-bottom: 2px;
export const DetailsText = (props: FlexProps) => (
<Flex
alignItems="center"
color={colors.dark.disabled.foreground}
{...props}
/>
);
const modalFooterShadowCss = css`
overflow: auto;
background: 0, linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%, 0,
linear-gradient(rgba(255, 255, 255, 0), rgba(221, 225, 240, 0.5) 70%) 0 100%;
background-repeat: no-repeat;
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
background-repeat: no-repeat;
background-color: white;
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
background-attachment: local, local, scroll, scroll;
`;
export const DetailsText = styled(Txt.p)`
color: ${colors.dark.disabled.foreground};
margin-bottom: 0;
export const Modal = styled(({ style, children, ...props }) => {
return (
<ModalBase
position="top"
width="97vw"
cancelButtonProps={{
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
},
}}
style={{
height: '87.5vh',
...style,
}}
{...props}
>
<ScrollableFlex flexDirection="column" width="100%" height="90%">
{...children}
</ScrollableFlex>
</ModalBase>
);
})`
> div {
padding: 0;
height: 99%;
> div:first-child {
height: 81%;
padding: 24px 30px 0;
}
> h3 {
margin: 0;
padding: 24px 30px 0;
height: 14.3%;
}
> div:first-child {
height: 81%;
padding: 24px 30px 0;
}
> div:nth-child(2) {
height: 61%;
padding: 0 30px;
${modalFooterShadowCss}
}
> div:last-child {
margin: 0;
flex-direction: ${(props) =>
props.reverseFooterButtons ? 'row-reverse' : 'row'};
border-radius: 0 0 7px 7px;
height: 80px;
background-color: #fff;
justify-content: center;
width: 100%;
}
::-webkit-scrollbar {
display: none;
}
}
`;
export const ScrollableFlex = styled(Flex)`
overflow: auto;
::-webkit-scrollbar {
display: none;
}
> div > div {
/* This is required for the sticky table header in TargetsTable */
overflow-x: visible;
}
`;
export const Alert = styled((props) => (
<AlertBase warning emphasized {...props}></AlertBase>
))`
position: fixed;
top: -40px;
left: 50%;
transform: translate(-50%, 0px);
height: 30px;
min-width: 50%;
padding: 0px;
justify-content: center;
align-items: center;
font-size: 14px;
background-color: #fca321;
text-align: center;
* {
color: #ffffff;
}
> div:first-child {
display: none;
}
`;
export interface GenericTableProps<T> extends BaseTableProps<T> {
refFn: (t: BaseTable<T>) => void;
data: T[];
checkedRowsNumber?: number;
multipleSelection: boolean;
showWarnings?: boolean;
}
const GenericTable: <T>(
props: GenericTableProps<T>,
) => React.ReactElement<GenericTableProps<T>> = <T extends {}>({
refFn,
...props
}: GenericTableProps<T>) => (
<div>
<BaseTable<T> ref={refFn} {...props} />
</div>
);
function StyledTable<T>() {
return styled((props: GenericTableProps<T>) => (
<GenericTable<T> {...props} />
))`
[data-display='table-head']
> [data-display='table-row']
> [data-display='table-cell'] {
position: sticky;
background-color: #f8f9fd;
top: 0;
z-index: 1;
input[type='checkbox'] + div {
display: ${(props) => (props.multipleSelection ? 'flex' : 'none')};
${(props) =>
props.multipleSelection &&
props.checkedRowsNumber !== 0 &&
props.checkedRowsNumber !== props.data.length
? `
font-weight: 600;
color: ${colors.primary.foreground};
background: ${colors.primary.background};
::after {
content: '';
}
`
: ''}
}
}
}
[data-display='table-head'] > [data-display='table-row'],
[data-display='table-body'] > [data-display='table-row'] {
> [data-display='table-cell']:first-child {
padding-left: 15px;
width: 6%;
}
> [data-display='table-cell']:last-child {
padding-right: 0;
}
}
[data-display='table-body'] > [data-display='table-row'] {
&:nth-of-type(2n) {
background: transparent;
}
&[data-highlight='true'] {
&.system {
background-color: ${(props) => (props.showWarnings ? '#fff5e6' : '#e8f5fc')};
}
> [data-display='table-cell']:first-child {
box-shadow: none;
}
}
}
&& [data-display='table-row'] > [data-display='table-cell'] {
padding: 6px 8px;
color: #2a506f;
}
input[type='checkbox'] + div {
border-radius: ${(props) => (props.multipleSelection ? '4px' : '50%')};
}
`;
}
export const Table = <T extends {}>(props: GenericTableProps<T>) => {
const TypedStyledFunctional = StyledTable<T>();
return <TypedStyledFunctional {...props} />;
};

View File

@@ -14,6 +14,9 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import { Theme } from 'rendition';
export const colors = {
dark: {
foreground: '#fff',
@@ -64,3 +67,60 @@ export const colors = {
background: '#5fb835',
},
};
const font = 'SourceSansPro';
export const theme = _.merge({}, Theme, {
colors,
font,
header: {
height: '40px',
},
global: {
font: {
family: font,
size: 16,
},
text: {
medium: {
size: 16,
},
},
},
button: {
border: {
width: '0',
radius: '24px',
},
disabled: {
opacity: 1,
},
extend: () => `
width: 200px;
font-size: 16px;
&& {
width: 200px;
height: 48px;
}
: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};
}
}
`,
},
layer: {
extend: () => `
> div:first-child {
background-color: transparent;
}
`,
},
});

View File

@@ -1,68 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 260.9 74" style="enable-background:new 0 0 260.9 74;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;}
.st2{fill:#FFFFFF;}
.st3{fill:#FFC100;}
.st4{fill:#F6EB61;}
.st5{fill:#439879;}
.st6{fill:#28CDFB;}
.st7{fill:#FDD757;}
.st8{fill:#EC8B00;}
</style>
<g id="type" class="st0">
<text transform="matrix(1 0 0 1 264.4807 53.6223)" class="st1" style="font-family:'ITCAvantGardeStd-Bold'; font-size:46.2px;">Fin</text>
</g>
<g id="Ebene_1">
<g>
<path class="st2" d="M88.8,19.7h6.7v11.1h0.1c0.7-1,1.7-1.7,2.9-2.3c1.2-0.5,2.5-0.9,3.8-1.1c0.3,0,0.7-0.1,1-0.1
c0.3,0,0.6,0,0.9,0c4.1,0,7.5,1.4,10.1,4.1c2.6,2.7,3.9,5.9,3.9,9.4c0,0.5,0,1.1-0.1,1.6c-0.1,0.6-0.2,1.1-0.4,1.7
c-0.3,1.1-0.7,2.2-1.2,3.2c-0.5,1-1.2,2-1.9,2.7c-1.2,1.4-2.8,2.4-4.6,3.1c-1.8,0.7-3.7,1.1-5.6,1.1c-1.9,0-3.7-0.3-5.3-1
c-1.6-0.7-3-1.7-4.1-3.2l-0.1,0v3.4h-6.2V19.7z M97.6,35.4c-1.7,1.4-2.5,3.1-2.5,5.2c0,2.2,0.8,4.1,2.3,5.6
c1.5,1.5,3.6,2.3,6.1,2.3c2.4,0,4.3-0.7,5.8-2.2c1.5-1.4,2.2-3.2,2.2-5.4c0-2.1-0.7-3.9-2.2-5.4c-1.5-1.5-3.4-2.2-5.8-2.2
C101.2,33.3,99.3,34,97.6,35.4z"/>
<path class="st2" d="M150.3,53.6h-6.2v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M144,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C143.2,44.9,144,43,144,40.8
L144,40.8z"/>
<path class="st2" d="M155.3,19.7h6.7v33.9h-6.7V19.7z"/>
<path class="st2" d="M173.3,43.6c0.5,1.5,1.4,2.7,2.8,3.6c1.4,0.9,2.9,1.3,4.6,1.3c1.3,0,2.5-0.2,3.6-0.6c1.1-0.4,2-0.9,2.6-1.6
l7.4,0c-0.8,2.3-2.5,4.2-5.1,5.8c-2.6,1.6-5.3,2.4-8.3,2.4c-4.1,0-7.5-1.3-10.4-3.9c-2.9-2.6-4.3-5.7-4.3-9.4c0-3.8,1.4-7,4.3-9.7
c2.9-2.7,6.4-4,10.5-4c4,0,7.4,1.3,10.2,4c2.8,2.7,4.2,5.8,4.2,9.3c0,0.4,0,0.8-0.1,1.2c-0.1,0.4-0.1,0.8-0.2,1.1
c0,0.1-0.1,0.2-0.1,0.3c0,0.1,0,0.2,0,0.3H173.3z M188.6,38.2c-0.5-1.5-1.5-2.7-2.9-3.5c-1.4-0.9-3-1.3-4.7-1.3
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c-1.6,0.1-3.1,0.6-4.5,1.4c-1.4,0.9-2.4,2-2.8,3.4H188.6z"/>
<path class="st2" d="M199.7,28.2h6.2v2.3h0.1c0.8-0.9,1.8-1.7,3-2.2c1.3-0.5,2.6-0.8,4-0.9c0.1,0,0.2,0,0.3,0c0.1,0,0.2,0,0.3,0
c0.1,0,0.3,0,0.4,0c0.1,0,0.3,0,0.4,0c1.3,0.1,2.6,0.4,3.9,1c1.3,0.5,2.3,1.3,3.3,2.2c0.1,0.1,0.3,0.2,0.4,0.3
c0.1,0.1,0.2,0.2,0.3,0.4c1.1,1.4,1.7,2.8,1.9,4.4s0.3,3.1,0.3,4.8v13.1h-6.7v-12c0-0.4,0-0.8,0-1.2c0-0.4,0-0.9-0.1-1.3
c-0.1-0.7-0.2-1.3-0.4-1.9c-0.2-0.6-0.4-1.2-0.8-1.7c-0.4-0.6-1-1.1-1.8-1.5c-0.8-0.4-1.5-0.6-2.3-0.6c0,0-0.1,0-0.1,0
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.3,0c-0.1,0-0.2,0-0.4,0c-0.8,0.1-1.5,0.3-2.3,0.7c-0.7,0.4-1.3,0.9-1.7,1.5
c-0.3,0.5-0.6,1.1-0.8,1.7c-0.2,0.7-0.3,1.3-0.3,2c0,0.4,0,0.8,0,1.2c0,0.4,0,0.8,0,1.1c0,0.1,0,0.2,0,0.3c0,0.1,0,0.1,0,0.2v11.5
h-6.7V28.2z"/>
<path class="st2" d="M258.2,53.6H252v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M251.8,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C251.1,44.9,251.8,43,251.8,40.8
L251.8,40.8z"/>
</g>
<g>
<path class="st3" d="M34.9,43.9v20.6c0.9-0.2,1.7-0.4,2.5-0.9l17.1-9.8c2.5-1.4,4-4.1,4-7V27.3c0-0.8-0.1-1.6-0.4-2.3L39.6,35.7
C35.7,38.4,34.9,40.9,34.9,43.9z"/>
<path class="st4" d="M64.9,21l-6.8,3.9c0.2,0.7,0.4,1.5,0.4,2.3v19.6c0,2.9-1.6,5.6-4,7l-17.1,9.8c-0.8,0.4-1.6,0.7-2.5,0.9v7.8
c1.2-0.2,2.4-0.6,3.4-1.2l22.2-12.7c3.1-1.8,5-5.1,5-8.7V24.3C65.5,23.2,65.3,22.1,64.9,21z"/>
<path class="st5" d="M33.3,37.4c1-1.6,2.5-3.1,4.7-4.4l18.7-10.8c-0.6-0.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1,0
l-17,9.8c-0.9,0.5-1.6,1.2-2.3,2L28.6,33C30.8,34.4,32.3,35.8,33.3,37.4z"/>
<path class="st6" d="M12.3,20.3l17-9.8c2.5-1.4,5.6-1.4,8.1,0l17.1,9.8c0.9,0.5,1.6,1.2,2.2,2l6.8-3.9c-0.8-1.1-1.8-2-3-2.6
L38.3,2.9c-3.1-1.8-6.9-1.8-10,0L6.3,15.7c-1.2,0.7-2.2,1.6-3,2.7l6.8,3.9C10.6,21.5,11.4,20.8,12.3,20.3z"/>
<path class="st7" d="M29.3,63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-0.8,0.1-1.5,0.3-2.2l-6.8-3.9c-0.4,1.1-0.6,2.1-0.6,3.2v25.5
c0,3.6,1.9,6.9,5,8.6l22.1,12.7c1,0.6,2.2,1,3.4,1.2v-7.8C30.9,64.4,30.1,64.1,29.3,63.6z"/>
<path class="st8" d="M27,35.6L8.6,25c-0.2,0.7-0.3,1.5-0.3,2.2v19.6c0,2.9,1.5,5.6,4,7l17,9.8c0.8,0.4,1.6,0.7,2.5,0.9V43.9
C31.7,40.9,30.9,38.4,27,35.6z"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260.9 74"><style>.st2{fill:#fff}</style><g id="Ebene_1"><path class="st2" d="M88.8 19.7h6.7v11.1h.1c.7-1 1.7-1.7 2.9-2.3 1.2-.5 2.5-.9 3.8-1.1.3 0 .7-.1 1-.1h.9c4.1 0 7.5 1.4 10.1 4.1 2.6 2.7 3.9 5.9 3.9 9.4 0 .5 0 1.1-.1 1.6-.1.6-.2 1.1-.4 1.7-.3 1.1-.7 2.2-1.2 3.2s-1.2 2-1.9 2.7c-1.2 1.4-2.8 2.4-4.6 3.1-1.8.7-3.7 1.1-5.6 1.1-1.9 0-3.7-.3-5.3-1-1.6-.7-3-1.7-4.1-3.2h-.1v3.4h-6.2V19.7zm8.8 15.7c-1.7 1.4-2.5 3.1-2.5 5.2 0 2.2.8 4.1 2.3 5.6 1.5 1.5 3.6 2.3 6.1 2.3 2.4 0 4.3-.7 5.8-2.2 1.5-1.4 2.2-3.2 2.2-5.4 0-2.1-.7-3.9-2.2-5.4-1.5-1.5-3.4-2.2-5.8-2.2-2.3 0-4.2.7-5.9 2.1zM150.3 53.6h-6.2v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zM144 40.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.4-1.4 2.2-3.3 2.2-5.5zM155.3 19.7h6.7v33.9h-6.7V19.7zM173.3 43.6c.5 1.5 1.4 2.7 2.8 3.6 1.4.9 2.9 1.3 4.6 1.3 1.3 0 2.5-.2 3.6-.6 1.1-.4 2-.9 2.6-1.6h7.4c-.8 2.3-2.5 4.2-5.1 5.8-2.6 1.6-5.3 2.4-8.3 2.4-4.1 0-7.5-1.3-10.4-3.9-2.9-2.6-4.3-5.7-4.3-9.4 0-3.8 1.4-7 4.3-9.7 2.9-2.7 6.4-4 10.5-4 4 0 7.4 1.3 10.2 4 2.8 2.7 4.2 5.8 4.2 9.3 0 .4 0 .8-.1 1.2-.1.4-.1.8-.2 1.1 0 .1-.1.2-.1.3v.3h-21.7zm15.3-5.4c-.5-1.5-1.5-2.7-2.9-3.5-1.4-.9-3-1.3-4.7-1.3h-.4c-1.6.1-3.1.6-4.5 1.4-1.4.9-2.4 2-2.8 3.4h15.3zM199.7 28.2h6.2v2.3h.1c.8-.9 1.8-1.7 3-2.2 1.3-.5 2.6-.8 4-.9h1.4c1.3.1 2.6.4 3.9 1 1.3.5 2.3 1.3 3.3 2.2.1.1.3.2.4.3.1.1.2.2.3.4 1.1 1.4 1.7 2.8 1.9 4.4s.3 3.1.3 4.8v13.1h-6.7v-12-1.2c0-.4 0-.9-.1-1.3-.1-.7-.2-1.3-.4-1.9-.2-.6-.4-1.2-.8-1.7-.4-.6-1-1.1-1.8-1.5-.8-.4-1.5-.6-2.3-.6h-1c-.8.1-1.5.3-2.3.7-.7.4-1.3.9-1.7 1.5-.3.5-.6 1.1-.8 1.7-.2.7-.3 1.3-.3 2v14.3h-6.7V28.2zM258.2 53.6H252v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zm-6.4-12.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.5-1.4 2.2-3.3 2.2-5.5z"/><g><path d="M34.9 43.9v20.6c.9-.2 1.7-.4 2.5-.9l17.1-9.8c2.5-1.4 4-4.1 4-7V27.3c0-.8-.1-1.6-.4-2.3L39.6 35.7c-3.9 2.7-4.7 5.2-4.7 8.2z" fill="#ffc100"/><path d="M64.9 21l-6.8 3.9c.2.7.4 1.5.4 2.3v19.6c0 2.9-1.6 5.6-4 7l-17.1 9.8c-.8.4-1.6.7-2.5.9v7.8c1.2-.2 2.4-.6 3.4-1.2l22.2-12.7c3.1-1.8 5-5.1 5-8.7V24.3c0-1.1-.2-2.2-.6-3.3z" fill="#f6eb61"/><path d="M33.3 37.4c1-1.6 2.5-3.1 4.7-4.4l18.7-10.8c-.6-.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1 0l-17 9.8c-.9.5-1.6 1.2-2.3 2L28.6 33c2.2 1.4 3.7 2.8 4.7 4.4z" fill="#439879"/><path d="M12.3 20.3l17-9.8c2.5-1.4 5.6-1.4 8.1 0l17.1 9.8c.9.5 1.6 1.2 2.2 2l6.8-3.9c-.8-1.1-1.8-2-3-2.6L38.3 2.9c-3.1-1.8-6.9-1.8-10 0l-22 12.8c-1.2.7-2.2 1.6-3 2.7l6.8 3.9c.5-.8 1.3-1.5 2.2-2z" fill="#28cdfb"/><path d="M29.3 63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-.8.1-1.5.3-2.2l-6.8-3.9c-.4 1.1-.6 2.1-.6 3.2v25.5c0 3.6 1.9 6.9 5 8.6l22.1 12.7c1 .6 2.2 1 3.4 1.2v-7.8c-.8-.1-1.6-.4-2.4-.9z" fill="#fdd757"/><path d="M27 35.6L8.6 25c-.2.7-.3 1.5-.3 2.2v19.6c0 2.9 1.5 5.6 4 7l17 9.8c.8.4 1.6.7 2.5.9V43.9c-.1-3-.9-5.5-4.8-8.3z" fill="#ec8b00"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,18 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 134.229 134.229" enable-background="new 0 0 134.229 134.229"
xml:space="preserve">
<g>
<g>
<path fill="#FFFFFF" d="M21.343,112.528c2.317,0,4.195,1.875,4.195,4.189c0,2.319-1.878,4.201-4.195,4.201
c-2.32,0-4.199-1.882-4.199-4.201C17.144,114.403,19.022,112.528,21.343,112.528z"/>
<path fill="#FFFFFF" d="M131.246,110.53L119.604,5.8C119.25,2.615,116.047,0,112.48,0H21.754c-3.568,0-6.777,2.615-7.127,5.8
L2.984,110.53c0,0.129-0.061,0.232-0.061,0.359v11.667c0,6.437,5.237,11.673,11.667,11.673h105.05
c6.431,0,11.667-5.236,11.667-11.673v-11.667C131.307,110.762,131.246,110.652,131.246,110.53z M125.474,122.556
c0,3.222-2.631,5.84-5.84,5.84H14.59c-3.206,0-5.836-2.618-5.836-5.84v-11.667c0-3.221,2.63-5.839,5.836-5.839h105.05
c3.203,0,5.834,2.618,5.834,5.839V122.556L125.474,122.556z"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 134.229 134.229"><g fill="#FFF"><path d="M21.343 112.528a4.192 4.192 0 014.195 4.189 4.199 4.199 0 01-4.195 4.201 4.2 4.2 0 01-4.199-4.201 4.192 4.192 0 014.199-4.189z"/><path d="M131.246 110.53L119.604 5.8c-.354-3.185-3.557-5.8-7.124-5.8H21.754c-3.568 0-6.777 2.615-7.127 5.8L2.984 110.53c0 .129-.061.232-.061.359v11.667c0 6.437 5.237 11.673 11.667 11.673h105.05c6.431 0 11.667-5.236 11.667-11.673v-11.667c0-.127-.061-.237-.061-.359zm-5.772 12.026c0 3.222-2.631 5.84-5.84 5.84H14.59c-3.206 0-5.836-2.618-5.836-5.84v-11.667c0-3.221 2.63-5.839 5.836-5.839h105.05c3.203 0 5.834 2.618 5.834 5.839v11.667z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 667 B

View File

@@ -1,79 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 412.1 74" style="enable-background:new 0 0 412.1 74;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;}
.st2{font-family:'ITCAvantGardeStd-Bold';}
.st3{font-size:46.2px;}
.st4{fill:#FFFFFF;}
.st5{fill:#A5DE37;}
.st6{fill:#C8F178;}
</style>
<g id="type" class="st0">
<text transform="matrix(1 0 0 1 264.4807 53.6223)" class="st1 st2 st3">Etcher</text>
</g>
<g id="Ebene_1">
<g>
<g>
<path class="st4" d="M88.8,19.7h6.7v11.1h0.1c0.7-1,1.7-1.7,2.9-2.3c1.2-0.5,2.5-0.9,3.8-1.1c0.3,0,0.7-0.1,1-0.1
c0.3,0,0.6,0,0.9,0c4.1,0,7.5,1.4,10.1,4.1c2.6,2.7,3.9,5.9,3.9,9.4c0,0.5,0,1.1-0.1,1.6c-0.1,0.6-0.2,1.1-0.4,1.7
c-0.3,1.1-0.7,2.2-1.2,3.2c-0.5,1-1.2,2-1.9,2.7c-1.2,1.4-2.8,2.4-4.6,3.1c-1.8,0.7-3.7,1.1-5.6,1.1c-1.9,0-3.7-0.3-5.3-1
c-1.6-0.7-3-1.7-4.1-3.2l-0.1,0v3.4h-6.2V19.7z M97.6,35.4c-1.7,1.4-2.5,3.1-2.5,5.2c0,2.2,0.8,4.1,2.3,5.6
c1.5,1.5,3.6,2.3,6.1,2.3c2.4,0,4.3-0.7,5.8-2.2c1.5-1.4,2.2-3.2,2.2-5.4c0-2.1-0.7-3.9-2.2-5.4c-1.5-1.5-3.4-2.2-5.8-2.2
C101.2,33.3,99.3,34,97.6,35.4z"/>
<path class="st4" d="M150.3,53.6h-6.2v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M144,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C143.2,44.9,144,43,144,40.8
L144,40.8z"/>
<path class="st4" d="M155.3,19.7h6.7v33.9h-6.7V19.7z"/>
<path class="st4" d="M173.3,43.6c0.5,1.5,1.4,2.7,2.8,3.6c1.4,0.9,2.9,1.3,4.6,1.3c1.3,0,2.5-0.2,3.6-0.6c1.1-0.4,2-0.9,2.6-1.6
l7.4,0c-0.8,2.3-2.5,4.2-5.1,5.8c-2.6,1.6-5.3,2.4-8.3,2.4c-4.1,0-7.5-1.3-10.4-3.9c-2.9-2.6-4.3-5.7-4.3-9.4
c0-3.8,1.4-7,4.3-9.7c2.9-2.7,6.4-4,10.5-4c4,0,7.4,1.3,10.2,4c2.8,2.7,4.2,5.8,4.2,9.3c0,0.4,0,0.8-0.1,1.2
c-0.1,0.4-0.1,0.8-0.2,1.1c0,0.1-0.1,0.2-0.1,0.3c0,0.1,0,0.2,0,0.3H173.3z M188.6,38.2c-0.5-1.5-1.5-2.7-2.9-3.5
c-1.4-0.9-3-1.3-4.7-1.3c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c-1.6,0.1-3.1,0.6-4.5,1.4c-1.4,0.9-2.4,2-2.8,3.4H188.6z"/>
<path class="st4" d="M199.7,28.2h6.2v2.3h0.1c0.8-0.9,1.8-1.7,3-2.2c1.3-0.5,2.6-0.8,4-0.9c0.1,0,0.2,0,0.3,0c0.1,0,0.2,0,0.3,0
c0.1,0,0.3,0,0.4,0c0.1,0,0.3,0,0.4,0c1.3,0.1,2.6,0.4,3.9,1c1.3,0.5,2.3,1.3,3.3,2.2c0.1,0.1,0.3,0.2,0.4,0.3
c0.1,0.1,0.2,0.2,0.3,0.4c1.1,1.4,1.7,2.8,1.9,4.4s0.3,3.1,0.3,4.8v13.1h-6.7v-12c0-0.4,0-0.8,0-1.2c0-0.4,0-0.9-0.1-1.3
c-0.1-0.7-0.2-1.3-0.4-1.9c-0.2-0.6-0.4-1.2-0.8-1.7c-0.4-0.6-1-1.1-1.8-1.5c-0.8-0.4-1.5-0.6-2.3-0.6c0,0-0.1,0-0.1,0
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.3,0c-0.1,0-0.2,0-0.4,0c-0.8,0.1-1.5,0.3-2.3,0.7c-0.7,0.4-1.3,0.9-1.7,1.5
c-0.3,0.5-0.6,1.1-0.8,1.7c-0.2,0.7-0.3,1.3-0.3,2c0,0.4,0,0.8,0,1.2c0,0.4,0,0.8,0,1.1c0,0.1,0,0.2,0,0.3c0,0.1,0,0.1,0,0.2
v11.5h-6.7V28.2z"/>
<path class="st4" d="M258.2,53.6H252v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M251.8,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C251.1,44.9,251.8,43,251.8,40.8
L251.8,40.8z"/>
</g>
<g>
<path class="st5" d="M34.9,43.9v20.6c0.9-0.2,1.7-0.4,2.5-0.9l17.1-9.8c2.5-1.4,4-4.1,4-7V27.3c0-0.8-0.1-1.6-0.4-2.3L39.6,35.7
C35.7,38.4,34.9,40.9,34.9,43.9z"/>
<path class="st6" d="M64.9,21l-6.8,3.9c0.2,0.7,0.4,1.5,0.4,2.3v19.6c0,2.9-1.6,5.6-4,7l-17.1,9.8c-0.8,0.4-1.6,0.7-2.5,0.9v7.8
c1.2-0.2,2.4-0.6,3.4-1.2l22.2-12.7c3.1-1.8,5-5.1,5-8.7V24.3C65.5,23.2,65.3,22.1,64.9,21z"/>
<path class="st5" d="M33.3,37.4c1-1.6,2.5-3.1,4.7-4.4l18.7-10.8c-0.6-0.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1,0
l-17,9.8c-0.9,0.5-1.6,1.2-2.3,2L28.6,33C30.8,34.4,32.3,35.8,33.3,37.4z"/>
<path class="st6" d="M12.3,20.3l17-9.8c2.5-1.4,5.6-1.4,8.1,0l17.1,9.8c0.9,0.5,1.6,1.2,2.2,2l6.8-3.9c-0.8-1.1-1.8-2-3-2.6
L38.3,2.9c-3.1-1.8-6.9-1.8-10,0L6.3,15.7c-1.2,0.7-2.2,1.6-3,2.7l6.8,3.9C10.6,21.5,11.4,20.8,12.3,20.3z"/>
<path class="st6" d="M29.3,63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-0.8,0.1-1.5,0.3-2.2l-6.8-3.9c-0.4,1.1-0.6,2.1-0.6,3.2v25.5
c0,3.6,1.9,6.9,5,8.6l22.1,12.7c1,0.6,2.2,1,3.4,1.2v-7.8C30.9,64.4,30.1,64.1,29.3,63.6z"/>
<path class="st5" d="M27,35.6L8.6,25c-0.2,0.7-0.3,1.5-0.3,2.2v19.6c0,2.9,1.5,5.6,4,7l17,9.8c0.8,0.4,1.6,0.7,2.5,0.9V43.9
C31.7,40.9,30.9,38.4,27,35.6z"/>
</g>
<path class="st5" d="M267.6,19.4h19.4v7.7h-10.6v5.3h10.3v7.7h-10.3V46h10.6v7.7h-19.4V19.4z"/>
<path class="st5" d="M294.3,33.8h-3.8V28h3.8v-8.5h7.7V28h3.7v5.8H302v19.8h-7.7V33.8z"/>
<path class="st5" d="M334.5,43.9c-1.4,5.8-6.5,10.6-13.4,10.6c-7.8,0-13.7-6.1-13.7-13.7c0-7.5,5.9-13.6,13.5-13.6
c6.8,0,12.3,4.5,13.6,10.8h-7.8c-0.8-1.8-2.4-3.6-5.5-3.6c-1.8-0.1-3.3,0.6-4.4,1.8c-1.1,1.2-1.7,2.9-1.7,4.7
c0,3.7,2.4,6.5,6.1,6.5c3.2,0,4.7-1.8,5.5-3.4H334.5z"/>
<path class="st5" d="M338,19.4h7.7v7.7v3.2c1.4-2.3,4-3.2,6.7-3.2c3.9,0,6.2,1.4,7.6,3.6c1.4,2.2,1.8,5.3,1.8,8.5v14.3h-7.7v-14
c0-1.4-0.2-2.8-0.8-3.7c-0.6-1-1.7-1.6-3.3-1.6c-2.1,0-3.2,1-3.7,2.1c-0.6,1.1-0.6,2.4-0.6,3v14.2H338V19.4z"/>
<path class="st5" d="M373.5,43.5c0.3,2.7,2.9,4.5,5.9,4.5c2.4,0,3.7-1.1,4.7-2.4h7.9c-1.2,2.9-3,5.1-5.2,6.6
c-2.1,1.5-4.7,2.3-7.3,2.3c-7.3,0-13.6-6-13.6-13.6c0-7.2,5.6-13.8,13.4-13.8c3.9,0,7.3,1.5,9.7,4.1c3.2,3.5,4.2,7.6,3.6,12.3
H373.5z M385,37.6c-0.2-1.2-1.8-4.1-5.7-4.1c-4,0-5.5,2.9-5.7,4.1H385z"/>
<path class="st5" d="M397,28h7.2v2.9c0.7-1.4,2.1-3.7,6.5-3.7v7.7h-0.3c-3.9,0-5.8,1.4-5.8,5v13.8H397V28z"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 412.1 74"><style>.st4{fill:#fff}.st5{fill:#a5de37}.st6{fill:#c8f178}</style><g id="Ebene_1"><path class="st4" d="M88.8 19.7h6.7v11.1h.1c.7-1 1.7-1.7 2.9-2.3 1.2-.5 2.5-.9 3.8-1.1.3 0 .7-.1 1-.1h.9c4.1 0 7.5 1.4 10.1 4.1 2.6 2.7 3.9 5.9 3.9 9.4 0 .5 0 1.1-.1 1.6-.1.6-.2 1.1-.4 1.7-.3 1.1-.7 2.2-1.2 3.2s-1.2 2-1.9 2.7c-1.2 1.4-2.8 2.4-4.6 3.1-1.8.7-3.7 1.1-5.6 1.1-1.9 0-3.7-.3-5.3-1-1.6-.7-3-1.7-4.1-3.2h-.1v3.4h-6.2V19.7zm8.8 15.7c-1.7 1.4-2.5 3.1-2.5 5.2 0 2.2.8 4.1 2.3 5.6 1.5 1.5 3.6 2.3 6.1 2.3 2.4 0 4.3-.7 5.8-2.2 1.5-1.4 2.2-3.2 2.2-5.4 0-2.1-.7-3.9-2.2-5.4-1.5-1.5-3.4-2.2-5.8-2.2-2.3 0-4.2.7-5.9 2.1zM150.3 53.6h-6.2v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zM144 40.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.4-1.4 2.2-3.3 2.2-5.5zM155.3 19.7h6.7v33.9h-6.7V19.7zM173.3 43.6c.5 1.5 1.4 2.7 2.8 3.6 1.4.9 2.9 1.3 4.6 1.3 1.3 0 2.5-.2 3.6-.6 1.1-.4 2-.9 2.6-1.6h7.4c-.8 2.3-2.5 4.2-5.1 5.8-2.6 1.6-5.3 2.4-8.3 2.4-4.1 0-7.5-1.3-10.4-3.9-2.9-2.6-4.3-5.7-4.3-9.4 0-3.8 1.4-7 4.3-9.7 2.9-2.7 6.4-4 10.5-4 4 0 7.4 1.3 10.2 4 2.8 2.7 4.2 5.8 4.2 9.3 0 .4 0 .8-.1 1.2-.1.4-.1.8-.2 1.1 0 .1-.1.2-.1.3v.3h-21.7zm15.3-5.4c-.5-1.5-1.5-2.7-2.9-3.5-1.4-.9-3-1.3-4.7-1.3h-.4c-1.6.1-3.1.6-4.5 1.4-1.4.9-2.4 2-2.8 3.4h15.3zM199.7 28.2h6.2v2.3h.1c.8-.9 1.8-1.7 3-2.2 1.3-.5 2.6-.8 4-.9h1.4c1.3.1 2.6.4 3.9 1 1.3.5 2.3 1.3 3.3 2.2.1.1.3.2.4.3.1.1.2.2.3.4 1.1 1.4 1.7 2.8 1.9 4.4s.3 3.1.3 4.8v13.1h-6.7v-12-1.2c0-.4 0-.9-.1-1.3-.1-.7-.2-1.3-.4-1.9-.2-.6-.4-1.2-.8-1.7-.4-.6-1-1.1-1.8-1.5-.8-.4-1.5-.6-2.3-.6h-1c-.8.1-1.5.3-2.3.7-.7.4-1.3.9-1.7 1.5-.3.5-.6 1.1-.8 1.7-.2.7-.3 1.3-.3 2v14.3h-6.7V28.2zM258.2 53.6H252v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zm-6.4-12.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.5-1.4 2.2-3.3 2.2-5.5z"/><path class="st5" d="M34.9 43.9v20.6c.9-.2 1.7-.4 2.5-.9l17.1-9.8c2.5-1.4 4-4.1 4-7V27.3c0-.8-.1-1.6-.4-2.3L39.6 35.7c-3.9 2.7-4.7 5.2-4.7 8.2z"/><path class="st6" d="M64.9 21l-6.8 3.9c.2.7.4 1.5.4 2.3v19.6c0 2.9-1.6 5.6-4 7l-17.1 9.8c-.8.4-1.6.7-2.5.9v7.8c1.2-.2 2.4-.6 3.4-1.2l22.2-12.7c3.1-1.8 5-5.1 5-8.7V24.3c0-1.1-.2-2.2-.6-3.3z"/><path class="st5" d="M33.3 37.4c1-1.6 2.5-3.1 4.7-4.4l18.7-10.8c-.6-.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1 0l-17 9.8c-.9.5-1.6 1.2-2.3 2L28.6 33c2.2 1.4 3.7 2.8 4.7 4.4z"/><path class="st6" d="M12.3 20.3l17-9.8c2.5-1.4 5.6-1.4 8.1 0l17.1 9.8c.9.5 1.6 1.2 2.2 2l6.8-3.9c-.8-1.1-1.8-2-3-2.6L38.3 2.9c-3.1-1.8-6.9-1.8-10 0l-22 12.8c-1.2.7-2.2 1.6-3 2.7l6.8 3.9c.5-.8 1.3-1.5 2.2-2zM29.3 63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-.8.1-1.5.3-2.2l-6.8-3.9c-.4 1.1-.6 2.1-.6 3.2v25.5c0 3.6 1.9 6.9 5 8.6l22.1 12.7c1 .6 2.2 1 3.4 1.2v-7.8c-.8-.1-1.6-.4-2.4-.9z"/><path class="st5" d="M27 35.6L8.6 25c-.2.7-.3 1.5-.3 2.2v19.6c0 2.9 1.5 5.6 4 7l17 9.8c.8.4 1.6.7 2.5.9V43.9c-.1-3-.9-5.5-4.8-8.3zM267.6 19.4H287v7.7h-10.6v5.3h10.3v7.7h-10.3V46H287v7.7h-19.4V19.4zM294.3 33.8h-3.8V28h3.8v-8.5h7.7V28h3.7v5.8H302v19.8h-7.7V33.8zM334.5 43.9c-1.4 5.8-6.5 10.6-13.4 10.6-7.8 0-13.7-6.1-13.7-13.7 0-7.5 5.9-13.6 13.5-13.6 6.8 0 12.3 4.5 13.6 10.8h-7.8c-.8-1.8-2.4-3.6-5.5-3.6-1.8-.1-3.3.6-4.4 1.8-1.1 1.2-1.7 2.9-1.7 4.7 0 3.7 2.4 6.5 6.1 6.5 3.2 0 4.7-1.8 5.5-3.4h7.8zM338 19.4h7.7v10.9c1.4-2.3 4-3.2 6.7-3.2 3.9 0 6.2 1.4 7.6 3.6 1.4 2.2 1.8 5.3 1.8 8.5v14.3h-7.7v-14c0-1.4-.2-2.8-.8-3.7-.6-1-1.7-1.6-3.3-1.6-2.1 0-3.2 1-3.7 2.1-.6 1.1-.6 2.4-.6 3v14.2H338V19.4zM373.5 43.5c.3 2.7 2.9 4.5 5.9 4.5 2.4 0 3.7-1.1 4.7-2.4h7.9c-1.2 2.9-3 5.1-5.2 6.6-2.1 1.5-4.7 2.3-7.3 2.3-7.3 0-13.6-6-13.6-13.6 0-7.2 5.6-13.8 13.4-13.8 3.9 0 7.3 1.5 9.7 4.1 3.2 3.5 4.2 7.6 3.6 12.3h-19.1zm11.5-5.9c-.2-1.2-1.8-4.1-5.7-4.1-4 0-5.5 2.9-5.7 4.1H385zM397 28h7.2v2.9c.7-1.4 2.1-3.7 6.5-3.7v7.7h-.3c-3.9 0-5.8 1.4-5.8 5v13.8H397V28z"/></g></svg>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,18 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="27px" height="40px" viewBox="0 0 27 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41.2 (35397) - http://www.bohemiancoding.com/sketch -->
<title>Combined Shape</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Steps" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Step-3/flash-image/flash-default-Copy" transform="translate(-692.000000, -168.000000)" fill="#FFFFFF">
<g id="main-UI" transform="translate(62.000000, 55.000000)">
<g id="Group-2" transform="translate(91.000000, 111.000000)">
<g id="Group-8" transform="translate(467.000000, 2.000000)">
<path d="M88.0046509,10.6971076 L93.1286727,0 L80.7751206,0 L72,18.3192841 L83.6485427,18.3192841 L80.1109889,40 L98.9145135,10.6334372 L88.0046509,10.6971076 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</g>
</g>
</svg>
<svg width="27" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M16.005 10.697L21.129 0H8.775L0 18.32h11.649L8.11 40l18.804-29.367-10.91.064z" fill="#FFF" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 190 B

View File

@@ -1,14 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 21 23" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title>Combined Shape</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="select-image/-category-copy-12" transform="translate(-407.000000, -109.000000)" fill-rule="nonzero" fill="#FFFFFF">
<g id="Group-7" transform="translate(407.000000, 109.000000)">
<path d="M21,7.51607355 L21,15.4875215 C21,16.7481894 20.3246037,17.9129891 19.2246726,18.5409264 L12.2777395,22.529047 C11.1778084,23.1569843 9.82701583,23.1569843 8.72708475,22.529047 L1.78015162,18.5409264 C0.680220536,17.9129891 0,16.7481894 0,15.4875215 L0,7.51607355 C0,6.2554056 0.680220536,5.09060595 1.78015162,4.46266868 L8.72708475,0.474548012 C9.82701583,-0.158182671 11.1778084,-0.158182671 12.2777395,0.474548012 L19.2246726,4.46266868 C20.3246037,5.09060595 21,6.2554056 21,7.51607355 Z M9.26574803,15.3966378 C9.26574803,16.0895512 9.80708661,16.6308898 10.5,16.6308898 C11.1712598,16.6308898 11.734252,16.0895512 11.734252,15.3966378 L11.734252,12.3867953 L14.8090551,12.3867953 C15.4586614,12.3867953 16,11.8671102 16,11.1958504 C16,10.5462441 15.4586614,10.0049055 14.8090551,10.0049055 L11.734252,10.0049055 L11.734252,6.99506299 C11.734252,6.30214961 11.1712598,5.76081102 10.5,5.76081102 C9.80708661,5.76081102 9.26574803,6.30214961 9.26574803,6.99506299 L9.26574803,10.0049055 L6.19094488,10.0049055 C5.54133858,10.0049055 5,10.5462441 5,11.1958504 C5,11.8671102 5.54133858,12.3867953 6.19094488,12.3867953 L9.26574803,12.3867953 L9.26574803,15.3966378 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</svg>
<svg viewBox="0 0 21 23" xmlns="http://www.w3.org/2000/svg"><path d="M21 7.516v7.972c0 1.26-.675 2.425-1.775 3.053l-6.947 3.988c-1.1.628-2.451.628-3.55 0L1.78 18.541A3.52 3.52 0 010 15.488V7.516c0-1.26.68-2.425 1.78-3.053L8.727.475a3.558 3.558 0 013.55 0l6.948 3.988A3.515 3.515 0 0121 7.516zm-11.734 7.88a1.22 1.22 0 001.234 1.235c.671 0 1.234-.541 1.234-1.234v-3.01h3.075c.65 0 1.191-.52 1.191-1.191 0-.65-.541-1.191-1.19-1.191h-3.076v-3.01c0-.693-.563-1.234-1.234-1.234a1.22 1.22 0 00-1.234 1.234v3.01H6.19c-.65 0-1.191.541-1.191 1.19 0 .672.541 1.192 1.19 1.192h3.076v3.01z" fill-rule="nonzero" fill="#FFF"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 618 B

View File

@@ -1,37 +0,0 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="#5793db">
<g fill="none" fill-rule="evenodd" stroke-width="2">
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="0s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="0s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="-0.9s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="-0.9s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,12 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 50 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>like</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Steps" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="like" fill-rule="nonzero" fill="#F55F50">
<path d="M24.85,8.126 C26.868,3.343 31.478,0.001 36.84,0.001 C44.063,0.001 49.265,6.18 49.919,13.544 C49.919,13.544 50.272,15.372 49.495,18.663 C48.437,23.145 45.95,27.127 42.597,30.166 L24.85,46 L7.402,30.165 C4.049,27.127 1.562,23.144 0.504,18.662 C-0.273,15.371 0.08,13.543 0.08,13.543 C0.734,6.179 5.936,0 13.159,0 C18.522,0 22.832,3.343 24.85,8.126 Z" id="Shape"></path>
</g>
</g>
</svg>
<svg viewBox="0 0 50 46" xmlns="http://www.w3.org/2000/svg"><path d="M24.85 8.126C26.868 3.343 31.478.001 36.84.001c7.223 0 12.425 6.179 13.079 13.543 0 0 .353 1.828-.424 5.119-1.058 4.482-3.545 8.464-6.898 11.503L24.85 46 7.402 30.165c-3.353-3.038-5.84-7.021-6.898-11.503-.777-3.291-.424-5.119-.424-5.119C.734 6.179 5.936 0 13.159 0c5.363 0 9.673 3.343 11.691 8.126z" fill-rule="nonzero" fill="#F55F50"/></svg>

Before

Width:  |  Height:  |  Size: 876 B

After

Width:  |  Height:  |  Size: 411 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

18
lib/gui/assets/src.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 39 90" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g transform="translate(-380 -166)">
<g transform="translate(380 166)">
<path d="m30.88 39.87h-23.363v23.209c0 0.6909 0.56062 1.251 1.251 1.251h20.861c0.69114 0 1.251-0.55986 1.251-1.251v-23.209zm-22.363 0.9999h21.363l4e-4 22.209c0 0.13886-0.11214 0.251-0.251 0.251h-20.861l-0.057452-0.0066403c-0.11075-0.026055-0.19355-0.12572-0.19355-0.24436l-4e-4 -22.209z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m16.558 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m25.97 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m37.398 5.418v30.534c0 2.43-1.988 4.418-4.418 4.418h-27.562c-2.43 0-4.418-1.988-4.418-4.418v-30.534c0-2.43 1.988-4.418 4.418-4.418h27.562c2.43 0 4.418 1.988 4.418 4.418" fill="#2A506F"/>
<path d="m32.98-5.6843e-14h-27.562c-2.9823 0-5.418 2.4357-5.418 5.418v30.534c0 2.9823 2.4357 5.418 5.418 5.418h27.562c2.9823 0 5.418-2.4357 5.418-5.418v-30.534c0-2.9823-2.4357-5.418-5.418-5.418zm-27.562 2h27.562c1.8777 0 3.418 1.5403 3.418 3.418v30.534c0 1.8777-1.5403 3.418-3.418 3.418h-27.562c-1.8777 0-3.418-1.5403-3.418-3.418v-30.534c0-1.8777 1.5403-3.418 3.418-3.418z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m19.147 73.551c0.24546 0 0.44961 0.17688 0.49194 0.41012l0.0080557 0.089876v14.882c0 0.27614-0.22386 0.5-0.5 0.5-0.24546 0-0.44961-0.17688-0.49194-0.41012l-0.0080557-0.089876v-14.882c0-0.27614 0.22386-0.5 0.5-0.5z" fill="#2A506F" fill-rule="nonzero"/>
<line x1="19.147" x2="14.532" y1="88.933" y2="84.214" stroke="#2A506F" stroke-linecap="round"/>
<line x1="19.147" x2="23.866" y1="88.933" y2="84.318" stroke="#2A506F" stroke-linecap="round"/>
<path d="m14.007 26.177c0.51076 0 0.96749-0.071211 1.3702-0.21363s0.74649-0.33887 1.0313-0.58934 0.50339-0.54268 0.65564-0.87664 0.22837-0.69247 0.22837-1.0755c0-0.3536-0.051567-0.66546-0.1547-0.93557s-0.2431-0.50585-0.4199-0.7072-0.38798-0.37816-0.63354-0.5304-0.50585-0.2873-0.78087-0.40517l-1.3702-0.58934c-0.19645-0.078578-0.38798-0.16452-0.5746-0.25783s-0.35851-0.20136-0.51567-0.32413-0.28239-0.2652-0.3757-0.42727-0.13997-0.36097-0.13997-0.5967c0-0.442 0.16452-0.78824 0.49357-1.0387s0.76368-0.3757 1.3039-0.3757c0.45182 0 0.85699 0.081034 1.2155 0.2431s0.6851 0.38552 0.97977 0.67037l0.663-0.7956c-0.34378-0.3536-0.76123-0.6409-1.2523-0.8619s-1.0264-0.3315-1.6059-0.3315c-0.442 0-0.84717 0.063845-1.2155 0.19153s-0.68756 0.30695-0.95767 0.53777-0.48129 0.50339-0.63354 0.8177-0.22837 0.65318-0.22837 1.0166c0 0.3536 0.058934 0.66546 0.1768 0.93557s0.27011 0.50339 0.45674 0.69984 0.3978 0.36342 0.63354 0.50094 0.46656 0.25538 0.69247 0.3536l1.3849 0.60407c0.22591 0.10804 0.43709 0.21118 0.63354 0.3094s0.36588 0.20872 0.5083 0.3315 0.25538 0.27011 0.33887 0.442 0.12523 0.38061 0.12523 0.62617c0 0.47147-0.1768 0.85208-0.5304 1.1418s-0.84963 0.43464-1.4881 0.43464c-0.50094 0-0.98468-0.1105-1.4512-0.3315s-0.87173-0.51321-1.2155-0.87664l-0.73667 0.85454c0.42236 0.442 0.92329 0.79069 1.5028 1.0461s1.2081 0.38307 1.8859 0.38307zm6.2664-0.1768v-4.5968c0.24556-0.60898 0.53286-1.0362 0.8619-1.2818s0.64581-0.36834 0.9503-0.36834c0.14733 0 0.27011 0.0098223 0.36834 0.029467s0.20627 0.049111 0.32413 0.0884l0.23573-1.0608c-0.22591-0.098223-0.48129-0.14733-0.76614-0.14733-0.41254 0-0.79315 0.1326-1.1418 0.3978s-0.64581 0.62371-0.89137 1.0755h-0.0442l-0.10313-1.2965h-1.0019v7.1604h1.2081zm6.5758 0.1768c0.43218 0 0.84471-0.081034 1.2376-0.2431s0.7514-0.38552 1.0755-0.67037l-0.5304-0.81034c-0.22591 0.19645-0.47884 0.36588-0.75877 0.5083s-0.58688 0.21363-0.92084 0.21363c-0.32413 0-0.62371-0.0663-0.89874-0.1989s-0.5083-0.31922-0.69984-0.55987-0.34132-0.52795-0.44937-0.8619-0.16207-0.7072-0.16207-1.1197 0.056478-0.78824 0.16943-1.1271 0.26766-0.63108 0.4641-0.87664 0.43218-0.43464 0.7072-0.56724 0.5746-0.1989 0.89874-0.1989c0.28485 0 0.54268 0.058934 0.7735 0.1768s0.44937 0.27011 0.65564 0.45674l0.6188-0.7956c-0.25538-0.22591-0.55005-0.42236-0.884-0.58934s-0.73667-0.25047-1.2081-0.25047c-0.46165 0-0.90119 0.083489-1.3186 0.25047s-0.78333 0.41254-1.0976 0.73667-0.56478 0.71948-0.7514 1.186-0.27993 0.99942-0.27993 1.5986c0 0.58934 0.085945 1.1173 0.25783 1.5838s0.40762 0.85945 0.7072 1.1787 0.65564 0.56232 1.0682 0.7293 0.85454 0.25047 1.326 0.25047z" fill="#fff" fill-rule="nonzero"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

1
lib/gui/assets/tgt.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="39" height="90"><g fill="none" fill-rule="evenodd"><path fill="#2A506F" fill-rule="nonzero" d="M31.38 39.87H8.017v23.21c0 .69.561 1.25 1.251 1.25H30.13a1.25 1.25 0 001.251-1.25V39.87zm-22.363 1H30.38v22.21a.25.25 0 01-.25.25H9.267l-.057-.007a.252.252 0 01-.194-.244V40.87z"/><path fill="#2A506F" fill-rule="nonzero" d="M17.058 48.925H13.09c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054H13.09a.055.055 0 01-.055-.054v-2.733c0-.03.024-.054.055-.054zm13.379-1h-3.967c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054h-3.967a.055.055 0 01-.055-.054v-2.733c0-.03.024-.054.055-.054z"/><path fill="#2A506F" d="M37.898 35.952a4.43 4.43 0 01-4.418 4.418H5.918A4.43 4.43 0 011.5 35.952V5.418A4.43 4.43 0 015.918 1H33.48a4.43 4.43 0 014.418 4.418v30.534z"/><path fill="#2A506F" fill-rule="nonzero" d="M33.48 0H5.918A5.431 5.431 0 00.5 5.418v30.534a5.431 5.431 0 005.418 5.418H33.48a5.431 5.431 0 005.418-5.418V5.418A5.431 5.431 0 0033.48 0zM5.918 2H33.48a3.43 3.43 0 013.418 3.418v30.534a3.43 3.43 0 01-3.418 3.418H5.918A3.43 3.43 0 012.5 35.952V5.418A3.43 3.43 0 015.918 2z"/><path fill="#FFF" fill-rule="nonzero" d="M14.067 25v-8.634h2.918v-1.031H9.913v1.031h2.917V25h1.237zm5.869 3.3a5.29 5.29 0 001.51-.199 3.765 3.765 0 001.142-.537c.314-.226.555-.489.722-.789.167-.3.25-.616.25-.95 0-.6-.208-1.034-.626-1.304-.417-.27-1.043-.405-1.878-.405H19.67c-.491 0-.825-.074-1.002-.221a.697.697 0 01-.265-.56c0-.196.044-.36.132-.493.089-.133.197-.253.324-.361.167.078.344.14.53.184.187.044.37.066.546.066a2.93 2.93 0 001.024-.177 2.55 2.55 0 00.832-.493c.236-.211.423-.472.56-.781.138-.31.207-.656.207-1.039 0-.304-.057-.584-.17-.84a2.068 2.068 0 00-.42-.633h1.474v-.928h-2.49a3.308 3.308 0 00-.465-.126 3.057 3.057 0 00-1.591.126 2.557 2.557 0 00-.862.508c-.245.22-.44.489-.582.803a2.547 2.547 0 00-.213 1.06c0 .433.096.813.287 1.142.192.33.405.592.641.789v.059a2.234 2.234 0 00-.53.53c-.167.226-.25.491-.25.796 0 .285.06.523.183.714a1.4 1.4 0 00.45.45v.059a2.93 2.93 0 00-.766.75c-.187.276-.28.566-.28.87 0 .315.07.59.213.825.143.236.344.437.604.604.26.167.572.293.936.376a5.37 5.37 0 001.208.125zm0-6.38a1.462 1.462 0 01-1.068-.456 1.528 1.528 0 01-.332-.538 2.052 2.052 0 01-.118-.714c0-.53.148-.94.442-1.23.295-.29.654-.435 1.076-.435.422 0 .78.145 1.076.434.294.29.442.7.442 1.23 0 .266-.04.504-.118.715a1.503 1.503 0 01-.818.877 1.462 1.462 0 01-.582.118zm.177 5.54c-.648 0-1.157-.112-1.525-.338-.368-.226-.553-.53-.553-.914 0-.206.06-.412.177-.619.118-.206.305-.402.56-.589.157.05.317.081.479.096.162.015.312.022.45.022h1.237c.471 0 .83.064 1.075.191.246.128.369.359.369.693 0 .186-.054.368-.162.545-.108.177-.26.332-.457.464-.197.133-.435.24-.715.324-.28.084-.591.125-.935.125zm7.077-2.283c.225 0 .454-.027.685-.081.23-.054.444-.116.64-.184l-.235-.914c-.118.05-.25.093-.398.133a1.619 1.619 0 01-.413.059c-.412 0-.7-.12-.861-.361-.162-.241-.244-.582-.244-1.024v-3.978h1.93v-.987h-1.93v-2.004h-1.016L25.2 17.84l-1.12.073v.914h1.06v3.963c0 .354.035.678.104.972.068.295.184.546.346.752.162.206.373.368.634.486.26.118.581.177.965.177z"/><path fill="#2A506F" fill-rule="nonzero" d="M19.647 73.55c.245 0 .45.178.492.41l.008.09v14.883a.5.5 0 01-.992.09l-.008-.09V74.05a.5.5 0 01.5-.5z"/><path fill="#2A506F" fill-rule="nonzero" d="M14.682 83.856a.5.5 0 01.639-.05l.068.058 4.615 4.719a.5.5 0 01-.646.757l-.068-.058-4.615-4.719a.5.5 0 01.007-.707z"/><path fill="#2A506F" fill-rule="nonzero" d="M24.016 83.96a.5.5 0 01.758.646l-.058.07-4.72 4.614a.5.5 0 01-.757-.646l.058-.069 4.72-4.615z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512.209 512.209" style="enable-background:new 0 0 512.209 512.209;" xml:space="preserve">
<g>
<path d="M507.345,439.683L288.084,37.688c-3.237-5.899-7.71-10.564-13.429-13.988c-5.705-3.427-11.893-5.142-18.554-5.142 s-12.85,1.718-18.558,5.142c-5.708,3.424-10.184,8.089-13.418,13.988L4.859,439.683c-6.663,11.998-6.473,23.989,0.57,35.98 c3.239,5.517,7.664,9.897,13.278,13.128c5.618,3.237,11.66,4.859,18.132,4.859h438.529c6.479,0,12.519-1.622,18.134-4.859 c5.62-3.23,10.038-7.611,13.278-13.128C513.823,463.665,514.015,451.681,507.345,439.683z M292.655,411.132 c0,2.662-0.91,4.897-2.71,6.704c-1.807,1.811-3.949,2.71-6.427,2.71h-54.816c-2.474,0-4.616-0.899-6.423-2.71 c-1.809-1.807-2.713-4.042-2.713-6.704v-54.248c0-2.662,0.905-4.897,2.713-6.704c1.807-1.811,3.946-2.71,6.423-2.71h54.812 c2.479,0,4.62,0.899,6.428,2.71c1.803,1.807,2.71,4.042,2.71,6.704v54.248H292.655z M292.088,304.357 c-0.198,1.902-1.198,3.47-3.001,4.709c-1.811,1.238-4.046,1.854-6.711,1.854h-52.82c-2.663,0-4.947-0.62-6.849-1.854 c-1.908-1.243-2.858-2.807-2.858-4.716l-4.853-130.47c0-2.667,0.953-4.665,2.856-5.996c2.474-2.093,4.758-3.14,6.854-3.14h62.809 c2.098,0,4.38,1.043,6.854,3.14c1.902,1.331,2.851,3.14,2.851,5.424L292.088,304.357z" fill="#D80027"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -14,25 +14,24 @@
* limitations under the License.
*/
import { delay } from 'bluebird';
import * as electron from 'electron';
import { autoUpdater } from 'electron-updater';
import { promises as fs } from 'fs';
import { platform } from 'os';
import * as _ from 'lodash';
import * as path from 'path';
import * as semver from 'semver';
import { packageType, version } from '../../package.json';
import * as EXIT_CODES from '../shared/exit-codes';
import { getConfig } from '../shared/utils';
import { delay, getConfig } from '../shared/utils';
import * as settings from './app/models/settings';
import * as analytics from './app/modules/analytics';
import { logException } from './app/modules/analytics';
import { buildWindowMenu } from './menu';
const customProtocol = 'etcher';
const scheme = `${customProtocol}://`;
const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
const packageUpdatable = _.includes(updatablePackageTypes, packageType);
const packageUpdatable = updatablePackageTypes.includes(packageType);
let packageUpdated = false;
async function checkForUpdates(interval: number) {
@@ -44,20 +43,30 @@ async function checkForUpdates(interval: number) {
const release = await autoUpdater.checkForUpdates();
const isOutdated =
semver.compare(release.updateInfo.version, version) > 0;
const shouldUpdate = release.updateInfo.stagingPercentage || 0 > 0;
const shouldUpdate = release.updateInfo.stagingPercentage !== 0; // undefinded (default) means 100%
if (shouldUpdate && isOutdated) {
await autoUpdater.downloadUpdate();
packageUpdated = true;
}
} catch (err) {
analytics.logException(err);
logException(err);
}
}
await delay(interval);
}
}
function getCommandLineURL(argv: string[]): string | undefined {
async function isFile(filePath: string): Promise<boolean> {
try {
const stat = await fs.stat(filePath);
return stat.isFile();
} catch {
// noop
}
return false;
}
async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
argv = argv.slice(electron.app.isPackaged ? 1 : 2);
if (argv.length) {
const value = argv[argv.length - 1];
@@ -69,6 +78,14 @@ function getCommandLineURL(argv: string[]): string | undefined {
if (platform() === 'darwin' && value.startsWith('-psn_')) {
return;
}
if (
!value.startsWith('http://') &&
!value.startsWith('https://') &&
!value.startsWith(scheme) &&
!(await isFile(value))
) {
return;
}
return value;
}
}
@@ -80,6 +97,7 @@ const sourceSelectorReady = new Promise((resolve) => {
async function selectImageURL(url?: string) {
// 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests
if (url !== undefined && url !== 'data:,') {
url = url.replace(/\/$/, ''); // on windows the url ends with an extra slash
url = url.startsWith(scheme) ? url.slice(scheme.length) : url;
await sourceSelectorReady;
electron.BrowserWindow.getAllWindows().forEach((window) => {
@@ -95,10 +113,18 @@ electron.app.on('open-url', async (event, data) => {
await selectImageURL(data);
});
interface AutoUpdaterConfig {
autoDownload?: boolean;
autoInstallOnAppQuit?: boolean;
allowPrerelease?: boolean;
fullChangelog?: boolean;
allowDowngrade?: boolean;
}
async function createMainWindow() {
const fullscreen = Boolean(await settings.get('fullscreen'));
const defaultWidth = 800;
const defaultHeight = 480;
const defaultWidth = settings.DEFAULT_WIDTH;
const defaultHeight = settings.DEFAULT_HEIGHT;
let width = defaultWidth;
let height = defaultHeight;
if (fullscreen) {
@@ -108,7 +134,7 @@ async function createMainWindow() {
width,
height,
frame: !fullscreen,
useContentSize: false,
useContentSize: true,
show: false,
resizable: false,
maximizable: false,
@@ -122,6 +148,7 @@ async function createMainWindow() {
webPreferences: {
backgroundThrottling: false,
nodeIntegration: true,
contextIsolation: false,
webviewTag: true,
zoomFactor: width / defaultWidth,
enableRemoteModule: true,
@@ -136,6 +163,9 @@ async function createMainWindow() {
// Prevent flash of white when starting the application
mainWindow.on('ready-to-show', () => {
console.timeEnd('ready-to-show');
// Electron sometimes caches the zoomFactor
// making it obnoxious to switch back-and-forth
mainWindow.webContents.setZoomFactor(width / defaultWidth);
mainWindow.show();
});
@@ -146,33 +176,36 @@ async function createMainWindow() {
event.preventDefault();
});
mainWindow.loadURL(`file://${path.join(__dirname, 'index.html')}`);
mainWindow.loadURL(
`file://${path.join(
'/',
...__dirname.split(path.sep).map(encodeURIComponent),
'index.html',
)}`,
);
const page = mainWindow.webContents;
page.once('did-frame-finish-load', async () => {
autoUpdater.on('error', (err) => {
analytics.logException(err);
logException(err);
});
if (packageUpdatable) {
try {
const onlineConfig = await getConfig();
const autoUpdaterConfig = _.get(
onlineConfig,
['autoUpdates', 'autoUpdaterConfig'],
{
autoDownload: false,
},
);
_.merge(autoUpdater, autoUpdaterConfig);
const checkForUpdatesTimer = _.get(
onlineConfig,
['autoUpdates', 'checkForUpdatesTimer'],
300000,
);
const configUrl = await settings.get('configUrl');
const onlineConfig = await getConfig(configUrl);
const autoUpdaterConfig: AutoUpdaterConfig = onlineConfig?.autoUpdates
?.autoUpdaterConfig ?? {
autoDownload: false,
};
for (const [key, value] of Object.entries(autoUpdaterConfig)) {
autoUpdater[key as keyof AutoUpdaterConfig] = value;
}
const checkForUpdatesTimer =
onlineConfig?.autoUpdates?.checkForUpdatesTimer ?? 300000;
checkForUpdates(checkForUpdatesTimer);
} catch (err) {
analytics.logException(err);
logException(err);
}
}
});
@@ -204,9 +237,9 @@ async function main(): Promise<void> {
window.restore();
}
window.focus();
await selectImageURL(getCommandLineURL(argv));
await selectImageURL(await getCommandLineURL(argv));
});
await selectImageURL(getCommandLineURL(process.argv));
await selectImageURL(await getCommandLineURL(process.argv));
}
}

View File

@@ -14,17 +14,31 @@
* limitations under the License.
*/
import { delay } from 'bluebird';
import { Drive as DrivelistDrive } from 'drivelist';
import * as sdk from 'etcher-sdk';
import {
BlockDevice,
File,
Http,
Metadata,
SourceDestination,
} from 'etcher-sdk/build/source-destination';
import {
MultiDestinationProgress,
OnProgressFunction,
OnFailFunction,
decompressThenFlash,
DECOMPRESSED_IMAGE_PREFIX,
} from 'etcher-sdk/build/multi-write';
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import * as _ from 'lodash';
import * as ipc from 'node-ipc';
import { totalmem } from 'os';
import { File, Http } from 'etcher-sdk/build/source-destination';
import { toJSON } from '../../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
import { SourceOptions } from '../app/components/source-selector/source-selector';
import { delay, isJson } from '../../shared/utils';
import { SourceMetadata } from '../app/components/source-selector/source-selector';
import axios from 'axios';
import * as _ from 'lodash';
ipc.config.id = process.env.IPC_CLIENT_ID as string;
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
@@ -55,8 +69,9 @@ function log(message: string) {
/**
* @summary Terminate the child writer process
*/
function terminate(exitCode: number) {
async function terminate(exitCode: number) {
ipc.disconnect(IPC_SERVER_ID);
await cleanupTmpFiles(Date.now(), DECOMPRESSED_IMAGE_PREFIX);
process.nextTick(() => {
process.exit(exitCode || SUCCESS);
});
@@ -68,17 +83,28 @@ function terminate(exitCode: number) {
async function handleError(error: Error) {
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
await delay(DISCONNECT_DELAY);
terminate(GENERAL_ERROR);
await terminate(GENERAL_ERROR);
}
interface WriteResult {
bytesWritten: number;
devices: {
export interface FlashError extends Error {
description: string;
device: string;
code: string;
}
export interface WriteResult {
bytesWritten?: number;
devices?: {
failed: number;
successful: number;
};
errors: Array<Error & { device: string }>;
sourceMetadata: sdk.sourceDestination.Metadata;
errors: FlashError[];
sourceMetadata?: Metadata;
}
export interface FlashResults extends WriteResult {
skip?: boolean;
cancelled?: boolean;
}
/**
@@ -100,26 +126,26 @@ async function writeAndValidate({
onProgress,
onFail,
}: {
source: sdk.sourceDestination.SourceDestination;
destinations: sdk.sourceDestination.BlockDevice[];
source: SourceDestination;
destinations: BlockDevice[];
verify: boolean;
autoBlockmapping: boolean;
decompressFirst: boolean;
onProgress: sdk.multiWrite.OnProgressFunction;
onFail: sdk.multiWrite.OnFailFunction;
onProgress: OnProgressFunction;
onFail: OnFailFunction;
}): Promise<WriteResult> {
const {
sourceMetadata,
failures,
bytesWritten,
} = await sdk.multiWrite.decompressThenFlash({
const { sourceMetadata, failures, bytesWritten } = await decompressThenFlash({
source,
destinations,
onFail,
onProgress,
verify,
trim: autoBlockmapping,
numBuffers: 32,
numBuffers: Math.min(
2 + (destinations.length - 1) * 32,
256,
Math.floor(totalmem() / 1024 ** 2 / 8),
),
decompressFirst,
});
const result: WriteResult = {
@@ -132,22 +158,22 @@ async function writeAndValidate({
sourceMetadata,
};
for (const [destination, error] of failures) {
const err = error as Error & { device: string };
err.device = (destination as sdk.sourceDestination.BlockDevice).device;
const err = error as FlashError;
const drive = destination as BlockDevice;
err.device = drive.device;
err.description = drive.description;
result.errors.push(err);
}
return result;
}
interface WriteOptions {
imagePath: string;
image: SourceMetadata;
destinations: DrivelistDrive[];
unmountOnSuccess: boolean;
validateWriteOnSuccess: boolean;
autoBlockmapping: boolean;
decompressFirst: boolean;
source: SourceOptions;
SourceType: string;
httpRequest?: any;
}
ipc.connectTo(IPC_SERVER_ID, () => {
@@ -160,22 +186,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
// no flashing information is available, then it will
// assume that the child died halfway through.
process.once('SIGINT', () => {
terminate(SUCCESS);
process.once('SIGINT', async () => {
await terminate(SUCCESS);
});
process.once('SIGTERM', () => {
terminate(SUCCESS);
process.once('SIGTERM', async () => {
await terminate(SUCCESS);
});
// The IPC server failed. Abort.
ipc.of[IPC_SERVER_ID].on('error', () => {
terminate(SUCCESS);
ipc.of[IPC_SERVER_ID].on('error', async () => {
await terminate(SUCCESS);
});
// The IPC server was disconnected. Abort.
ipc.of[IPC_SERVER_ID].on('disconnect', () => {
terminate(SUCCESS);
ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
await terminate(SUCCESS);
});
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
@@ -185,7 +211,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
* @example
* writer.on('progress', onProgress)
*/
const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => {
const onProgress = (state: MultiDestinationProgress) => {
ipc.of[IPC_SERVER_ID].emit('state', state);
};
@@ -200,11 +226,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
log('Abort');
ipc.of[IPC_SERVER_ID].emit('abort');
await delay(DISCONNECT_DELAY);
terminate(exitCode);
await terminate(exitCode);
};
const onSkip = async () => {
log('Skip validation');
ipc.of[IPC_SERVER_ID].emit('skip');
await delay(DISCONNECT_DELAY);
await terminate(exitCode);
};
ipc.of[IPC_SERVER_ID].on('cancel', onAbort);
ipc.of[IPC_SERVER_ID].on('skip', onSkip);
/**
* @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination
@@ -212,10 +247,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
* @example
* writer.on('fail', onFail)
*/
const onFail = (
destination: sdk.sourceDestination.BlockDevice,
error: Error,
) => {
const onFail = (destination: SourceDestination, error: Error) => {
ipc.of[IPC_SERVER_ID].emit('fail', {
// TODO: device should be destination
// @ts-ignore (destination.drive is private)
@@ -224,49 +256,69 @@ ipc.connectTo(IPC_SERVER_ID, () => {
});
};
const destinations = _.map(options.destinations, 'device');
log(`Image: ${options.imagePath}`);
const destinations = options.destinations.map((d) => d.device);
const imagePath = options.image.path;
log(`Image: ${imagePath}`);
log(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Validate on success: ${options.validateWriteOnSuccess}`);
log(`Auto blockmapping: ${options.autoBlockmapping}`);
log(`Decompress first: ${options.decompressFirst}`);
const dests = _.map(options.destinations, (destination) => {
return new sdk.sourceDestination.BlockDevice({
const dests = options.destinations.map((destination) => {
return new BlockDevice({
drive: destination,
unmountOnSuccess: options.unmountOnSuccess,
unmountOnSuccess: true,
write: true,
direct: true,
});
});
const { SourceType } = options;
let source;
if (SourceType === File.name) {
source = new File({
path: options.imagePath,
});
} else {
source = new Http({ url: options.imagePath, avoidRandomAccess: true });
}
try {
let source;
if (options.image.drive) {
source = new BlockDevice({
drive: options.image.drive,
direct: !options.autoBlockmapping,
});
} else {
if (SourceType === File.name) {
source = new File({
path: imagePath,
});
} else {
const decodedImagePath = decodeURIComponent(imagePath);
if (isJson(decodedImagePath)) {
const imagePathObject = JSON.parse(decodedImagePath);
source = new Http({
url: imagePathObject.url,
avoidRandomAccess: true,
axiosInstance: axios.create(_.omit(imagePathObject, ['url'])),
auth: options.image.auth,
});
} else {
source = new Http({
url: imagePath,
avoidRandomAccess: true,
auth: options.image.auth,
});
}
}
}
const results = await writeAndValidate({
source,
destinations: dests,
verify: options.validateWriteOnSuccess,
verify: true,
autoBlockmapping: options.autoBlockmapping,
decompressFirst: options.decompressFirst,
onProgress,
onFail,
});
log(`Finish: ${results.bytesWritten}`);
results.errors = _.map(results.errors, (error) => {
results.errors = results.errors.map((error) => {
return toJSON(error);
});
ipc.of[IPC_SERVER_ID].emit('done', { results });
await delay(DISCONNECT_DELAY);
terminate(exitCode);
} catch (error) {
log(`Error: ${error.message}`);
await terminate(exitCode);
} catch (error: any) {
exitCode = GENERAL_ERROR;
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
}

View File

@@ -5,9 +5,9 @@ ObjC.import('stdlib')
const app = Application.currentApplication()
app.includeStandardAdditions = true
const result = app.displayDialog('balenaEtcher wants to make changes. Type your password to allow this.', {
const result = app.displayDialog('balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.', {
defaultAnswer: '',
withIcon: 'stop',
withIcon: 'caution',
buttons: ['Cancel', 'Ok'],
defaultButton: 'Ok',
hiddenAnswer: true,

View File

@@ -15,11 +15,12 @@
*/
import { execFile } from 'child_process';
import { app, remote } from 'electron';
import { join } from 'path';
import { env } from 'process';
import { promisify } from 'util';
import { getAppPath } from '../utils';
const execFileAsync = promisify(execFile);
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
@@ -37,7 +38,7 @@ export async function sudo(
env: {
PATH: env.PATH,
SUDO_ASKPASS: join(
(app || remote.app).getAppPath(),
getAppPath(),
__dirname,
'sudo-askpass.osascript.js',
),
@@ -49,7 +50,7 @@ export async function sudo(
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
stderr,
};
} catch (error) {
} catch (error: any) {
if (error.code === 1) {
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
return { cancelled: true };

View File

@@ -14,41 +14,40 @@
* limitations under the License.
*/
import { Drive as DrivelistDrive } from 'drivelist';
import { Drive } from 'drivelist';
import * as _ from 'lodash';
import * as pathIsInside from 'path-is-inside';
import * as prettyBytes from 'pretty-bytes';
import * as messages from './messages';
import { SourceMetadata } from '../gui/app/components/source-selector/source-selector';
/**
* @summary The default unknown size for things such as images and drives
*/
const UNKNOWN_SIZE = 0;
/**
* @summary Check if a drive is locked
*
* @description
* This usually points out a locked SD Card.
*/
export function isDriveLocked(drive: DrivelistDrive): boolean {
return Boolean(_.get(drive, ['isReadOnly'], false));
}
export type DrivelistDrive = Drive & {
disabled: boolean;
name: string;
path: string;
logo: string;
displayName: string;
};
/**
* @summary Check if a drive is a system drive
*/
export function isSystemDrive(drive: DrivelistDrive): boolean {
return Boolean(_.get(drive, ['isSystem'], false));
return Boolean(drive.isSystem);
}
export interface Image {
path?: string;
isSizeEstimated?: boolean;
compressedSize?: number;
recommendedDriveSize?: number;
size?: number;
function sourceIsInsideDrive(source: string, drive: DrivelistDrive) {
for (const mountpoint of drive.mountpoints || []) {
if (pathIsInside(source, mountpoint.path)) {
return true;
}
}
return false;
}
/**
@@ -60,11 +59,14 @@ export interface Image {
*/
export function isSourceDrive(
drive: DrivelistDrive,
image: Image = {},
selection?: SourceMetadata,
): boolean {
for (const mountpoint of drive.mountpoints || []) {
if (image.path !== undefined && pathIsInside(image.path, mountpoint.path)) {
return true;
if (selection) {
if (selection.drive) {
return selection.drive.device === drive.device;
}
if (selection.path) {
return sourceIsInsideDrive(selection.path, drive);
}
}
return false;
@@ -74,17 +76,21 @@ export function isSourceDrive(
* @summary Check if a drive is large enough for an image
*/
export function isDriveLargeEnough(
drive: DrivelistDrive | undefined,
image: Image,
drive: DrivelistDrive,
image?: SourceMetadata,
): boolean {
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
const driveSize = drive.size || UNKNOWN_SIZE;
if (_.get(image, ['isSizeEstimated'])) {
if (image === undefined) {
return true;
}
if (image.isSizeEstimated) {
// If the drive size is smaller than the original image size, and
// the final image size is just an estimation, then we stop right
// here, based on the assumption that the final size will never
// be less than the original size.
if (driveSize < _.get(image, ['compressedSize'], UNKNOWN_SIZE)) {
if (driveSize < (image.compressedSize || UNKNOWN_SIZE)) {
return false;
}
@@ -95,25 +101,22 @@ export function isDriveLargeEnough(
return true;
}
return driveSize >= _.get(image, ['size'], UNKNOWN_SIZE);
return driveSize >= (image.size || UNKNOWN_SIZE);
}
/**
* @summary Check if a drive is disabled (i.e. not ready for selection)
* @summary Check if a drive is valid, i.e. large enough for an image
*/
export function isDriveDisabled(drive: DrivelistDrive): boolean {
return _.get(drive, ['disabled'], false);
}
/**
* @summary Check if a drive is valid, i.e. not locked and large enough for an image
*/
export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
export function isDriveValid(
drive: DrivelistDrive,
image?: SourceMetadata,
write: boolean = true,
): boolean {
return (
!isDriveLocked(drive) &&
isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image) &&
!isDriveDisabled(drive)
!write ||
(!drive.disabled &&
isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image as SourceMetadata))
);
}
@@ -124,23 +127,23 @@ export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
* If the image doesn't have a recommended size, this function returns true.
*/
export function isDriveSizeRecommended(
drive: DrivelistDrive | undefined,
image: Image,
drive: DrivelistDrive,
image?: SourceMetadata,
): boolean {
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
return driveSize >= _.get(image, ['recommendedDriveSize'], UNKNOWN_SIZE);
const driveSize = drive.size || UNKNOWN_SIZE;
return driveSize >= (image?.recommendedDriveSize || UNKNOWN_SIZE);
}
/**
* @summary 64GB
* @summary 128GB
*/
export const LARGE_DRIVE_SIZE = 64e9;
export const LARGE_DRIVE_SIZE = 128e9;
/**
* @summary Check whether a drive's size is 'large'
*/
export function isDriveSizeLarge(drive?: DrivelistDrive): boolean {
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
export function isDriveSizeLarge(drive: DrivelistDrive): boolean {
const driveSize = drive.size || UNKNOWN_SIZE;
return driveSize > LARGE_DRIVE_SIZE;
}
@@ -155,6 +158,33 @@ export const COMPATIBILITY_STATUS_TYPES = {
ERROR: 2,
};
export const statuses = {
locked: {
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.locked(),
},
system: {
type: COMPATIBILITY_STATUS_TYPES.WARNING,
message: messages.compatibility.system(),
},
containsImage: {
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.containsImage(),
},
large: {
type: COMPATIBILITY_STATUS_TYPES.WARNING,
message: messages.compatibility.largeDrive(),
},
small: {
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.tooSmall(),
},
sizeNotRecommended: {
type: COMPATIBILITY_STATUS_TYPES.WARNING,
message: messages.compatibility.sizeNotRecommended(),
},
};
/**
* @summary Get drive/image compatibility in an object
*
@@ -167,54 +197,42 @@ export const COMPATIBILITY_STATUS_TYPES = {
*/
export function getDriveImageCompatibilityStatuses(
drive: DrivelistDrive,
image: Image = {},
image: SourceMetadata | undefined,
write: boolean,
) {
const statusList = [];
// Mind the order of the if-statements if you modify.
if (isSourceDrive(drive, image)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.containsImage(),
});
} else if (isDriveLocked(drive)) {
if (drive.isReadOnly && write) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.locked(),
});
} else if (
}
if (
!_.isNil(drive) &&
!_.isNil(drive.size) &&
!isDriveLargeEnough(drive, image)
) {
const imageSize = (image.isSizeEstimated
? image.compressedSize
: image.size) as number;
const relativeBytes = imageSize - drive.size;
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
});
statusList.push(statuses.small);
} else {
// Avoid showing "large drive" with "system drive" status
if (isSystemDrive(drive)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.WARNING,
message: messages.compatibility.system(),
});
statusList.push(statuses.system);
} else if (isDriveSizeLarge(drive)) {
statusList.push(statuses.large);
}
if (isDriveSizeLarge(drive)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.WARNING,
message: messages.compatibility.largeDrive(),
});
if (isSourceDrive(drive, image as SourceMetadata)) {
statusList.push(statuses.containsImage);
}
if (!_.isNil(drive) && !isDriveSizeRecommended(drive, image)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.WARNING,
message: messages.compatibility.sizeNotRecommended(),
});
if (
image !== undefined &&
!_.isNil(drive) &&
!isDriveSizeRecommended(drive, image)
) {
statusList.push(statuses.sizeNotRecommended);
}
}
@@ -230,10 +248,11 @@ export function getDriveImageCompatibilityStatuses(
*/
export function getListDriveImageCompatibilityStatuses(
drives: DrivelistDrive[],
image: Image,
image: SourceMetadata | undefined,
write: boolean,
) {
return _.flatMap(drives, (drive) => {
return getDriveImageCompatibilityStatuses(drive, image);
return drives.flatMap((drive) => {
return getDriveImageCompatibilityStatuses(drive, image, write);
});
}
@@ -245,31 +264,15 @@ export function getListDriveImageCompatibilityStatuses(
*/
export function hasDriveImageCompatibilityStatus(
drive: DrivelistDrive,
image: Image,
image: SourceMetadata | undefined,
write: boolean,
) {
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
return Boolean(
getDriveImageCompatibilityStatuses(drive, image, write).length,
);
}
/**
* @summary Does any drive/image pair have at least one compatibility status?
* @function
* @public
*
* @description
* Given an image and a drive, return whether they have a connected compatibility status object.
*
* @param {Object[]} drives - drives
* @param {Object} image - image
* @returns {Boolean}
*
* @example
* if (constraints.hasDriveImageCompatibilityStatus(drive, image)) {
* console.log('This drive-image pair has a compatibility status message!')
* }
*/
export function hasListDriveImageCompatibilityStatus(
drives: DrivelistDrive[],
image: Image,
) {
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
export interface DriveStatus {
message: string;
type: number;
}

View File

@@ -14,48 +14,37 @@
* limitations under the License.
*/
import * as _ from 'lodash';
function createErrorDetails(options: {
title: string | ((error: Error) => string);
description: string | ((error: Error) => string);
}): {
title: (error: Error) => string;
description: (error: Error) => string;
} {
return _.pick(
_.mapValues(options, (value) => {
return _.isFunction(value) ? value : _.constant(value);
}),
['title', 'description'],
);
}
export type ErrorWithPath = Error & {
path?: string;
code?: keyof typeof HUMAN_FRIENDLY;
};
/**
* @summary Human-friendly error messages
*/
export const HUMAN_FRIENDLY = {
ENOENT: createErrorDetails({
title: (error: Error & { path: string }) => {
ENOENT: {
title: (error: ErrorWithPath) => {
return `No such file or directory: ${error.path}`;
},
description: "The file you're trying to access doesn't exist",
}),
EPERM: createErrorDetails({
title: "You're not authorized to perform this operation",
description: 'Please ensure you have necessary permissions for this task',
}),
EACCES: createErrorDetails({
title: "You don't have access to this resource",
description:
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: createErrorDetails({
title: 'Your system ran out of memory',
description:
},
ENOMEM: {
title: () => 'Your system ran out of memory',
description: () =>
'Please make sure your system has enough available memory for this task',
}),
};
},
} as const;
/**
* @summary Get user friendly property from an error
@@ -71,19 +60,21 @@ export const HUMAN_FRIENDLY = {
* }
*/
function getUserFriendlyMessageProperty(
error: Error,
error: ErrorWithPath,
property: 'title' | 'description',
): string | null {
const code = _.get(error, ['code']);
if (_.isNil(code) || !_.isString(code)) {
return null;
): string | undefined {
if (typeof error.code !== 'string') {
return undefined;
}
return _.invoke(HUMAN_FRIENDLY, [code, property], error);
return HUMAN_FRIENDLY[error.code]?.[property]?.(error);
}
const isBlank = _.flow([_.trim, _.isEmpty]);
function isBlank(s: string | number | null | undefined) {
if (typeof s === 'number') {
s = s.toString();
}
return (s ?? '').trim() === '';
}
/**
* @summary Get the title of an error
@@ -92,23 +83,19 @@ const isBlank = _.flow([_.trim, _.isEmpty]);
* Try to get as much information as possible about the error
* rather than falling back to generic messages right away.
*/
export function getTitle(error: Error): string {
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
return _.toString(error);
}
export function getTitle(error: ErrorWithPath): string {
const codeTitle = getUserFriendlyMessageProperty(error, 'title');
if (!_.isNil(codeTitle)) {
if (codeTitle !== undefined) {
return codeTitle;
}
const message = _.get(error, ['message']);
const message = error.message;
if (!isBlank(message)) {
return message;
}
const code = _.get(error, ['code']);
if (!_.isNil(code) && !isBlank(code)) {
const code = error.code;
if (!isBlank(code)) {
return `Error code: ${code}`;
}
@@ -119,40 +106,19 @@ export function getTitle(error: Error): string {
* @summary Get the description of an error
*/
export function getDescription(
error: Error & { description?: string },
options: { userFriendlyDescriptionsOnly?: boolean } = {},
error: ErrorWithPath & { description?: string },
): string {
_.defaults(options, {
userFriendlyDescriptionsOnly: false,
});
if (!_.isError(error) && !_.isPlainObject(error)) {
return '';
}
if (!isBlank(error.description)) {
return error.description as string;
}
const codeDescription = getUserFriendlyMessageProperty(error, 'description');
if (!_.isNil(codeDescription)) {
if (codeDescription !== undefined) {
return codeDescription;
}
if (options.userFriendlyDescriptionsOnly) {
return '';
}
if (error.stack) {
return error.stack;
}
if (_.isEmpty(error)) {
return '';
}
const INDENTATION_SPACES = 2;
return JSON.stringify(error, null, INDENTATION_SPACES);
return JSON.stringify(error, null, 2);
}
/**
@@ -162,24 +128,24 @@ export function createError(options: {
title: string;
description?: string;
report?: boolean;
code?: string;
}): Error & { description?: string; report?: boolean; code?: string } {
code?: keyof typeof HUMAN_FRIENDLY;
}): ErrorWithPath & { description?: string; report?: boolean } {
if (isBlank(options.title)) {
throw new Error(`Invalid error title: ${options.title}`);
}
const error: Error & {
const error: ErrorWithPath & {
description?: string;
report?: boolean;
code?: string;
} = new Error(options.title);
error.description = options.description;
if (!_.isNil(options.report) && !options.report) {
if (options.report === false) {
error.report = false;
}
if (!_.isNil(options.code)) {
if (options.code !== undefined) {
error.code = options.code;
}
@@ -198,7 +164,7 @@ export function createError(options: {
export function createUserError(options: {
title: string;
description: string;
code?: string;
code?: keyof typeof HUMAN_FRIENDLY;
}): Error {
return createError({
title: options.title,
@@ -208,13 +174,6 @@ export function createUserError(options: {
});
}
/**
* @summary Check if an error is an user error
*/
export function isUserError(error: Error & { report?: boolean }): boolean {
return _.isNil(error.report) ? false : !error.report;
}
/**
* @summary Convert an Error object to a JSON object
* @function
@@ -260,5 +219,5 @@ export function toJSON(
* @summary Convert a JSON object to an Error object
*/
export function fromJSON(json: any): Error {
return _.assign(new Error(json.message), json);
return Object.assign(new Error(json.message), json);
}

View File

@@ -15,16 +15,18 @@
*/
import { Dictionary } from 'lodash';
import { outdent } from 'outdent';
import * as prettyBytes from 'pretty-bytes';
export const progress: Dictionary<(quantity: number) => string> = {
successful: (quantity: number) => {
const plural = quantity === 1 ? '' : 's';
return `Successful device${plural}`;
return `Successful target${plural}`;
},
failed: (quantity: number) => {
const plural = quantity === 1 ? '' : 's';
return `Failed device${plural}`;
return `Failed target${plural}`;
},
};
@@ -53,11 +55,11 @@ export const info = {
export const compatibility = {
sizeNotRecommended: () => {
return 'Not Recommended';
return 'Not recommended';
},
tooSmall: (additionalSpace: string) => {
return `Insufficient space, additional ${additionalSpace} required`;
tooSmall: () => {
return 'Too small';
},
locked: () => {
@@ -65,28 +67,25 @@ export const compatibility = {
},
system: () => {
return 'System Drive';
return 'System drive';
},
containsImage: () => {
return 'Drive Mountpoint Contains Image';
return 'Source drive';
},
// The drive is large and therefore likely not a medium you want to write to.
largeDrive: () => {
return 'Large Drive';
return 'Large drive';
},
} as const;
export const warning = {
unrecommendedDriveSize: (
image: { recommendedDriveSize: number },
drive: { device: string; size: number },
) => {
return [
`This image recommends a ${image.recommendedDriveSize}`,
`bytes drive, however ${drive.device} is only ${drive.size} bytes.`,
].join(' ');
tooSmall: (source: { size: number }, target: { size: number }) => {
return outdent({ newline: ' ' })`
The selected source is ${prettyBytes(source.size - target.size)}
larger than this drive.
`;
},
exitWhileFlashing: () => {
@@ -115,11 +114,24 @@ export const warning = {
].join(' ');
},
largeDriveSize: (drive: { description: string; device: string }) => {
return [
`Drive ${drive.description} (${drive.device}) is unusually large for an SD card or USB stick.`,
'\n\nAre you sure you want to flash this drive?',
].join(' ');
driveMissingPartitionTable: () => {
return outdent({ newline: ' ' })`
It looks like this is not a bootable drive.
The drive does not appear to contain a partition table,
and might not be recognized or bootable by your device.
`;
},
largeDriveSize: () => {
return "This is a large drive! Make sure it doesn't contain files that you want to keep.";
},
systemDrive: () => {
return 'Selecting your system drive is dangerous and will erase your drive!';
},
sourceDrive: () => {
return 'Contains the image you chose to flash';
},
};
@@ -143,15 +155,12 @@ export const error = {
].join(' ');
},
openImage: (imageBasename: string, errorMessage: string) => {
return [
`Something went wrong while opening ${imageBasename}\n\n`,
`Error: ${errorMessage}`,
].join('');
},
openSource: (sourceName: string, errorMessage: string) => {
return outdent`
Something went wrong while opening ${sourceName}
elevationRequired: () => {
return 'This should should be run with root/administrator permissions.';
Error: ${errorMessage}
`;
},
flashFailure: (

View File

@@ -14,25 +14,41 @@
* limitations under the License.
*/
import * as Bluebird from 'bluebird';
import * as childProcess from 'child_process';
import { withTmpFile } from 'etcher-sdk/build/tmp';
import { promises as fs } from 'fs';
import * as _ from 'lodash';
import * as os from 'os';
import * as semver from 'semver';
import * as sudoPrompt from 'sudo-prompt';
import * as sudoPrompt from '@balena/sudo-prompt';
import { promisify } from 'util';
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
import * as errors from './errors';
import { tmpFileDisposer } from './utils';
const execAsync = promisify(childProcess.exec);
const execFileAsync = promisify(childProcess.execFile);
// sudo-prompt's exec callback is function(error, stdout, stderr) so we need multiArgs
const sudoExecAsync = Bluebird.promisify(sudoPrompt.exec, {
multiArgs: true,
}) as (cmd: string, options: any) => Bluebird<[string, string]>;
type Std = string | Buffer | undefined;
function sudoExecAsync(
cmd: string,
options: { name: string },
): Promise<{ stdout: Std; stderr: Std }> {
return new Promise((resolve, reject) => {
sudoPrompt.exec(
cmd,
options,
(error: Error | undefined, stdout: Std, stderr: Std) => {
if (error) {
reject(error);
} else {
resolve({ stdout, stderr });
}
},
);
});
}
/**
* @summary The user id of the UNIX "superuser"
@@ -46,7 +62,7 @@ export async function isElevated(): Promise<boolean> {
// See http://stackoverflow.com/a/28268802
try {
await execAsync('fltmc');
} catch (error) {
} catch (error: any) {
if (error.code === os.constants.errno.EPERM) {
return false;
}
@@ -108,17 +124,12 @@ export function createLaunchScript(
async function elevateScriptWindows(
path: string,
): Promise<{ cancelled: boolean }> {
// 'elevator' 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
// @ts-ignore this is a native module
const { elevate } = await import('../../build/Release/elevator.node');
const elevateAsync = promisify(elevate);
name: string,
): Promise<{ cancelled: false }> {
// '&' 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 cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')].join(' ');
await sudoExecAsync(cmd, { name });
return { cancelled: false };
}
async function elevateScriptUnix(
@@ -137,7 +148,7 @@ async function elevateScriptCatalina(
try {
const { cancelled } = await catalinaSudo(cmd);
return { cancelled };
} catch (error) {
} catch (error: any) {
throw errors.createError({ title: error.stderr });
}
}
@@ -161,15 +172,16 @@ export async function elevateCommand(
command.slice(1),
options.environment,
);
return Bluebird.using(
tmpFileDisposer({
return await withTmpFile(
{
keepOpen: false,
prefix: 'balena-etcher-electron-',
postfix: '.cmd',
}),
},
async ({ path }) => {
await fs.writeFile(path, launchScript);
if (isWindows) {
return elevateScriptWindows(path);
return elevateScriptWindows(path, options.applicationName);
}
if (
os.platform() === 'darwin' &&
@@ -180,7 +192,7 @@ export async function elevateCommand(
}
try {
return await elevateScriptUnix(path, options.applicationName);
} catch (error) {
} catch (error: any) {
// 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

View File

@@ -14,44 +14,28 @@
* limitations under the License.
*/
import * as sdk from 'etcher-sdk';
import * as mime from 'mime-types';
import * as path from 'path';
import { basename } from 'path';
export function getCompressedExtensions(): string[] {
const result = [];
for (const [
mimetype,
cls,
// @ts-ignore (mimetypes is private)
] of sdk.sourceDestination.SourceDestination.mimetypes.entries()) {
if (cls.prototype instanceof sdk.sourceDestination.CompressedSource) {
const extension = mime.extension(mimetype);
if (extension) {
result.push(extension);
}
}
}
return result;
}
export function getNonCompressedExtensions(): string[] {
return sdk.sourceDestination.SourceDestination.imageExtensions;
}
export function getArchiveExtensions(): string[] {
return ['zip', 'etch'];
}
export function getAllExtensions(): string[] {
return [
...getArchiveExtensions(),
...getNonCompressedExtensions(),
...getCompressedExtensions(),
];
}
export const SUPPORTED_EXTENSIONS = [
'bin',
'bz2',
'dmg',
'dsk',
'etch',
'gz',
'hddimg',
'img',
'iso',
'raw',
'rpi-sdimg',
'sdcard',
'vhd',
'wic',
'xz',
'zip',
];
export function looksLikeWindowsImage(imagePath: string): boolean {
const regex = /windows|win7|win8|win10|winxp/i;
return regex.test(path.basename(imagePath));
return regex.test(basename(imagePath));
}

View File

@@ -14,18 +14,8 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import * as prettyBytes from 'pretty-bytes';
const MEGABYTE_TO_BYTE_RATIO = 1000000;
export function bytesToMegabytes(bytes: number): number {
return bytes / MEGABYTE_TO_BYTE_RATIO;
}
export function bytesToClosestUnit(bytes: number): string | null {
if (_.isNumber(bytes)) {
return prettyBytes(bytes);
}
return null;
}

View File

@@ -14,19 +14,14 @@
* 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 axios from 'axios';
import { app, remote } from 'electron';
import { Dictionary } from 'lodash';
import * as errors from './errors';
import * as settings from '../gui/app/models/settings';
const getAsync = promisify(request.get);
export function isValidPercentage(percentage: any): boolean {
return _.every([_.isNumber(percentage), percentage >= 0, percentage <= 100]);
return typeof percentage === 'number' && percentage >= 0 && percentage <= 100;
}
export function percentageToFloat(percentage: any) {
@@ -38,62 +33,40 @@ export function percentageToFloat(percentage: any) {
return percentage / 100;
}
/**
* @summary Check if obj has one or many specific props
*/
export function hasProps(obj: _.Dictionary<any>, props: string[]): boolean {
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(): Promise<_.Dictionary<any>> {
const configUrl =
(await settings.get('configUrl')) ||
'https://balena.io/etcher/static/config.json';
return (await getAsync({ url: configUrl, json: true })).body;
export async function getConfig(configUrl?: string): Promise<Dictionary<any>> {
configUrl = configUrl ?? 'https://balena.io/etcher/static/config.json';
const response = await axios.get(configUrl, { responseType: 'json' });
return response.data;
}
/**
* @summary returns { path: String, cleanup: Function }
*
* @example
* const {path, cleanup } = await tmpFileAsync()
* console.log(path)
* cleanup()
*/
function tmpFileAsync(
options: tmp.FileOptions,
): Promise<{ path: string; cleanup: () => void }> {
return new Promise((resolve, reject) => {
tmp.file(options, (error, path, _fd, cleanup) => {
if (error) {
reject(error);
} else {
resolve({ path, cleanup });
}
});
export async function delay(duration: number): Promise<void> {
await new Promise((resolve) => {
setTimeout(resolve, duration);
});
}
/**
* @summary Disposer for tmpFileAsync, calls cleanup()
*
* @returns {Disposer<{ path: String, cleanup: Function }>}
*
* @example
* await Bluebird.using(tmpFileDisposer(), ({ path }) => {
* console.log(path);
* })
*/
export function tmpFileDisposer(
options: tmp.FileOptions,
): Bluebird.Disposer<{ path: string; cleanup: () => void }> {
return Bluebird.resolve(tmpFileAsync(options)).disposer(({ cleanup }) => {
cleanup();
});
export function getAppPath(): string {
return (
(app || remote.app)
.getAppPath()
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
// include the app-x64 or app-arm64 folder depending on the arch.
// We don't care about the app.asar file, we want the actual folder.
.replace(/\.asar$/, () =>
process.platform === 'darwin' ? '-' + process.arch : '',
)
);
}
export function isJson(jsonString: string) {
try {
JSON.parse(jsonString);
} catch (e) {
return false;
}
return true;
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2016 balena.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.
*/
// See http://electron.atom.io/docs/v0.37.7/api/environment-variables/#electronrunasnode
//
// Notice that if running electron with `ELECTRON_RUN_AS_NODE`, the binary
// *won't* attempt to load the `app.asar` application by default, therefore
// if passing `ELECTRON_RUN_AS_NODE`, you have to pass the path to the asar
// or the entry point file (this file) manually as an argument.
import { env } from 'process';
if (env.ELECTRON_RUN_AS_NODE) {
import('./gui/modules/child-writer');
} else {
import('./gui/etcher');
}

15063
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

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