Compare commits
539 Commits
v1.5.73
...
vipul/add-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
82d0bba96a | ||
![]() |
5945ab1f50 | ||
![]() |
59d67220d4 | ||
![]() |
61610ded84 | ||
![]() |
c87a132f40 | ||
![]() |
350d4de32b | ||
![]() |
f5f9025d6d | ||
![]() |
549d744d04 | ||
![]() |
6194460dc2 | ||
![]() |
8370f638b4 | ||
![]() |
ac34c51125 | ||
![]() |
b241470fe1 | ||
![]() |
179697040c | ||
![]() |
335766ed12 | ||
![]() |
4c5d052a71 | ||
![]() |
86423342a8 | ||
![]() |
d8b41552e3 | ||
![]() |
11c65fb392 | ||
![]() |
bed126506f | ||
![]() |
f6aeb52b16 | ||
![]() |
a5201942b8 | ||
![]() |
c1f7164273 | ||
![]() |
6774bf784c | ||
![]() |
56ec8b4eac | ||
![]() |
35868509af | ||
![]() |
3ab6749f49 | ||
![]() |
7a012a92bc | ||
![]() |
aba01825a0 | ||
![]() |
907a3308de | ||
![]() |
4366bb372f | ||
![]() |
a6f6cd4a19 | ||
![]() |
03ee428039 | ||
![]() |
8d652d064d | ||
![]() |
28adc34239 | ||
![]() |
120e9bf42f | ||
![]() |
59f54e194b | ||
![]() |
c4834e61a7 | ||
![]() |
e4d02bc561 | ||
![]() |
b9e54e39f7 | ||
![]() |
f3c32eac65 | ||
![]() |
9a303ab344 | ||
![]() |
9c1b55bebc | ||
![]() |
30ae4bbd86 | ||
![]() |
c6126a980a | ||
![]() |
ef90d048ca | ||
![]() |
b938132038 | ||
![]() |
3cb2e78fe7 | ||
![]() |
ea9875ddf0 | ||
![]() |
65dacd2ff2 | ||
![]() |
a190818827 | ||
![]() |
98e33b619b | ||
![]() |
685ed715ac | ||
![]() |
3cf3c4b398 | ||
![]() |
1c2ef4b1d4 | ||
![]() |
d22fc91585 | ||
![]() |
0a28af5c35 | ||
![]() |
0c1e5b88ef | ||
![]() |
790201be90 | ||
![]() |
d8d379f05e | ||
![]() |
b5e9701048 | ||
![]() |
292f86d6f5 | ||
![]() |
76ca9934c8 | ||
![]() |
37b826ee4e | ||
![]() |
1e1bd3c508 | ||
![]() |
00e8f11913 | ||
![]() |
a3c24a26a0 | ||
![]() |
4232928ad8 | ||
![]() |
b165fb78da | ||
![]() |
e9f6c5ead9 | ||
![]() |
b2d0c1c9dd | ||
![]() |
14d91400a4 | ||
![]() |
d0114aece7 | ||
![]() |
dff2df4aab | ||
![]() |
13159f93ee | ||
![]() |
3ece1fd841 | ||
![]() |
f46963b6b3 | ||
![]() |
b97f4e0031 | ||
![]() |
e2d233d74b | ||
![]() |
a7ca2e527b | ||
![]() |
396a053c0a | ||
![]() |
d1a3f1cb88 | ||
![]() |
9f96558cdd | ||
![]() |
b3bc589d70 | ||
![]() |
18d2c28110 | ||
![]() |
b272ef296d | ||
![]() |
32ca28a3a9 | ||
![]() |
4d5e5a3b0b | ||
![]() |
8b3f37102d | ||
![]() |
4b74253631 | ||
![]() |
a81b552b95 | ||
![]() |
53f53c0f75 | ||
![]() |
fdaf5c69d6 | ||
![]() |
061afca5d3 | ||
![]() |
ccb08a48f1 | ||
![]() |
a8f3d45b12 | ||
![]() |
7e333caaf9 | ||
![]() |
70229e8684 | ||
![]() |
261700389b | ||
![]() |
250aed2eb1 | ||
![]() |
ed1f008fe2 | ||
![]() |
e9ce270dab | ||
![]() |
1ee110bc95 | ||
![]() |
33dd07c675 | ||
![]() |
39ccbbeeda | ||
![]() |
55d2400ac7 | ||
![]() |
0bdea5c54c | ||
![]() |
3be372d49f | ||
![]() |
d0c66b2c48 | ||
![]() |
65082c4790 | ||
![]() |
e87ed9beed | ||
![]() |
bc5563d9c2 | ||
![]() |
ad83ab5dcc | ||
![]() |
0dc1cf9701 | ||
![]() |
11489c6538 | ||
![]() |
2619d4bc86 | ||
![]() |
3730efd350 | ||
![]() |
6ece32c546 | ||
![]() |
fd9996a3cc | ||
![]() |
f06cc89152 | ||
![]() |
c1d7ab3fa9 | ||
![]() |
b206483c7c | ||
![]() |
c3eb8c7b56 | ||
![]() |
0849d4f435 | ||
![]() |
1dba3ae19b | ||
![]() |
f33f2e3771 | ||
![]() |
e56aaed973 | ||
![]() |
a4659f038e | ||
![]() |
cd462818da | ||
![]() |
37769efbed | ||
![]() |
0f70c4bbce | ||
![]() |
48b5e8b9d9 | ||
![]() |
1f138f0ecc | ||
![]() |
73f67e99ca | ||
![]() |
9114da2445 | ||
![]() |
554bbcc780 | ||
![]() |
4db2289cfd | ||
![]() |
c15b56bc23 | ||
![]() |
9f52dda6ae | ||
![]() |
fadcefb11a | ||
![]() |
361c32913c | ||
![]() |
5c2042198e | ||
![]() |
99df53098c | ||
![]() |
aa563c87bd | ||
![]() |
1188888956 | ||
![]() |
f9d7991dc8 | ||
![]() |
53954e81fd | ||
![]() |
f82996bfd1 | ||
![]() |
b74069eb41 | ||
![]() |
e8c7591751 | ||
![]() |
3521b61a81 | ||
![]() |
93db90c725 | ||
![]() |
1dc56aed14 | ||
![]() |
d814202424 | ||
![]() |
c54856a616 | ||
![]() |
fc45df270a | ||
![]() |
3cde2faed0 | ||
![]() |
b4b8c89aad | ||
![]() |
36d05724c0 | ||
![]() |
b1e4e681d1 | ||
![]() |
3987078c11 | ||
![]() |
de0010eb72 | ||
![]() |
1f94f44b18 | ||
![]() |
fe0b45cae6 | ||
![]() |
c32e485f27 | ||
![]() |
409b78fc21 | ||
![]() |
2f08142f5a | ||
![]() |
8c4edaabba | ||
![]() |
05497ce85c | ||
![]() |
d3df2fe57e | ||
![]() |
a0f07082f2 | ||
![]() |
b7efa8e1f0 | ||
![]() |
3647457bb5 | ||
![]() |
2e5a39dcd8 | ||
![]() |
edabacfb3a | ||
![]() |
f46176fd10 | ||
![]() |
2158e20380 | ||
![]() |
fa593e33d1 | ||
![]() |
50730bd3df | ||
![]() |
4e68955981 | ||
![]() |
3c0084d012 | ||
![]() |
8bd11a01ae | ||
![]() |
da3a22d0f6 | ||
![]() |
e708212d41 | ||
![]() |
a5ceba8435 | ||
![]() |
446e8e1253 | ||
![]() |
c69b2fa053 | ||
![]() |
0597c0e908 | ||
![]() |
af2b6bc8ca | ||
![]() |
a2c7a542df | ||
![]() |
e37ae2743f | ||
![]() |
644d955f08 | ||
![]() |
e7b4f09021 | ||
![]() |
1e0a6a3129 | ||
![]() |
ef3b8915d8 | ||
![]() |
e58cfd89c5 | ||
![]() |
1c52379ee3 | ||
![]() |
e2c2b40690 | ||
![]() |
bddb89e4a1 | ||
![]() |
560ed91e2e | ||
![]() |
1f8f7ad7f8 | ||
![]() |
a2a0f2ef41 | ||
![]() |
40e5fb2287 | ||
![]() |
6c49c71b3f | ||
![]() |
deb3db0fff | ||
![]() |
4872fa3d6e | ||
![]() |
640a7409ee | ||
![]() |
a7637ad8d4 | ||
![]() |
31409c61ca | ||
![]() |
e74dc9eb60 | ||
![]() |
06997fdf29 | ||
![]() |
611e659626 | ||
![]() |
e484ae9837 | ||
![]() |
7e7ca9524e | ||
![]() |
db09b7440d | ||
![]() |
e9603505d2 | ||
![]() |
0f45f6aca1 | ||
![]() |
0a28a7794d | ||
![]() |
7c2644ec51 | ||
![]() |
ae62812c61 | ||
![]() |
68e24df52b | ||
![]() |
b9076d01af | ||
![]() |
78a5339e3e | ||
![]() |
b099770cb1 | ||
![]() |
b76366a514 | ||
![]() |
eeab351636 | ||
![]() |
3e45691d0b | ||
![]() |
f9d79521a1 | ||
![]() |
14a89b3b8a | ||
![]() |
8fa6e618c4 | ||
![]() |
093008dee7 | ||
![]() |
42838eba09 | ||
![]() |
aa72c5d3bb | ||
![]() |
bb04098062 | ||
![]() |
dda022df37 | ||
![]() |
377dfb8e22 | ||
![]() |
07befd0bd1 | ||
![]() |
2635a410df | ||
![]() |
5e5f82c4b5 | ||
![]() |
991cbf6b7f | ||
![]() |
688d697a99 | ||
![]() |
7894a67719 | ||
![]() |
7a7ea74984 | ||
![]() |
12cd8a39c1 | ||
![]() |
2c07538f8f | ||
![]() |
c9bfd350ed | ||
![]() |
a485d2b4df | ||
![]() |
8ed5ff25a5 | ||
![]() |
a17a919c37 | ||
![]() |
55cafb9268 | ||
![]() |
92dfdc6edd | ||
![]() |
fff9452509 | ||
![]() |
27e560c961 | ||
![]() |
34489f0d66 | ||
![]() |
b7f8c8368c | ||
![]() |
f383f0be6c | ||
![]() |
ff08cb44f9 | ||
![]() |
6cb914e969 | ||
![]() |
a24be20e95 | ||
![]() |
08716efbd5 | ||
![]() |
24c8ede746 | ||
![]() |
548475996c | ||
![]() |
7f9add3f1e | ||
![]() |
6eab47259e | ||
![]() |
46663e3a6f | ||
![]() |
9797a2152d | ||
![]() |
a7c3431556 | ||
![]() |
fef9cd7bec | ||
![]() |
b2c4f7a250 | ||
![]() |
88ae9fcbd1 | ||
![]() |
bc092114c1 | ||
![]() |
9f29dc8b76 | ||
![]() |
5fbaa3a3db | ||
![]() |
0c59168ceb | ||
![]() |
540fe90609 | ||
![]() |
1f44f3944f | ||
![]() |
fbacb8187d | ||
![]() |
ac2d4ae8f3 | ||
![]() |
a3322e9fd7 | ||
![]() |
281f119456 | ||
![]() |
140f3452ed | ||
![]() |
481be42eb5 | ||
![]() |
f2a37079eb | ||
![]() |
76fa698995 | ||
![]() |
f8e21e2338 | ||
![]() |
482c29bc2a | ||
![]() |
0bf1ec4958 | ||
![]() |
3b105d5a6a | ||
![]() |
6d9c81da43 | ||
![]() |
c2e23855b3 | ||
![]() |
3f59d35fb6 | ||
![]() |
44c74f33d9 | ||
![]() |
512785e0a9 | ||
![]() |
963fc574c3 | ||
![]() |
3218fc2c83 | ||
![]() |
dc9351713c | ||
![]() |
e72049d6e8 | ||
![]() |
170126a490 | ||
![]() |
7d53d0aadc | ||
![]() |
5eac622b8c | ||
![]() |
175e41de8d | ||
![]() |
61f4762341 | ||
![]() |
7c24d1486f | ||
![]() |
630f6c691c | ||
![]() |
5c5273bd6c | ||
![]() |
9bde38df5a | ||
![]() |
391e4444d4 | ||
![]() |
e5ee0f1961 | ||
![]() |
c8737806c0 | ||
![]() |
953f572b53 | ||
![]() |
05d0f7142d | ||
![]() |
ba29d76a00 | ||
![]() |
692274691e | ||
![]() |
394d3e0bf2 | ||
![]() |
784dd03ba7 | ||
![]() |
8560189a1e | ||
![]() |
098ca9a9a1 | ||
![]() |
3ca50a1e2d | ||
![]() |
00f193541d | ||
![]() |
8ce9eac704 | ||
![]() |
76086a8f91 | ||
![]() |
9b71772e35 | ||
![]() |
72e5631167 | ||
![]() |
339c7d56bd | ||
![]() |
ba16995070 | ||
![]() |
b32c4ee728 | ||
![]() |
14e4cbf749 | ||
![]() |
406955ca3e | ||
![]() |
5a45f8b122 | ||
![]() |
129e7e20e8 | ||
![]() |
7165a8190b | ||
![]() |
07fde0d73f | ||
![]() |
a360370c4e | ||
![]() |
92cd3d688d | ||
![]() |
6554ccf0f8 | ||
![]() |
9444f0e1b1 | ||
![]() |
d63f5eca0d | ||
![]() |
e39fed1f25 | ||
![]() |
2dc359b19c | ||
![]() |
7aec8a4ae2 | ||
![]() |
af9d3ba9f1 | ||
![]() |
b0c71b21b3 | ||
![]() |
71c7fbd3a2 | ||
![]() |
f8cc7c36b4 | ||
![]() |
5d95fcb81f | ||
![]() |
d481536a3f | ||
![]() |
62b42e9254 | ||
![]() |
03e3354d50 | ||
![]() |
f01f1ddd7a | ||
![]() |
2cb58bbbf0 | ||
![]() |
2aedea3139 | ||
![]() |
59e37182be | ||
![]() |
52bdd02a4b | ||
![]() |
b1376dfa73 | ||
![]() |
37ed18c38b | ||
![]() |
b7ad7bd729 | ||
![]() |
b43ec4414e | ||
![]() |
f05f9d33f9 | ||
![]() |
fcc9c5e577 | ||
![]() |
3259a8206f | ||
![]() |
3fa9611971 | ||
![]() |
b749c2d45a | ||
![]() |
29e2e9c657 | ||
![]() |
f983d88e52 | ||
![]() |
1449478c5b | ||
![]() |
7e7a669116 | ||
![]() |
28f9954661 | ||
![]() |
b7e82f7694 | ||
![]() |
f0bbd1a1cd | ||
![]() |
5f5c66e3f2 | ||
![]() |
2fc8b07e29 | ||
![]() |
bdb1690a49 | ||
![]() |
10b028355f | ||
![]() |
a4366556c0 | ||
![]() |
9c25cc663a | ||
![]() |
ba21da4f0b | ||
![]() |
34349f64d5 | ||
![]() |
f5c7dc932a | ||
![]() |
4880275e7b | ||
![]() |
6db0172a50 | ||
![]() |
95ff5c98a8 | ||
![]() |
e9f9f90137 | ||
![]() |
0ebfecc60c | ||
![]() |
afa29a0ed1 | ||
![]() |
8d707dc815 | ||
![]() |
5b509d147f | ||
![]() |
bb6d909949 | ||
![]() |
8513d63a3e | ||
![]() |
d2f3345c7a | ||
![]() |
aee3a0a281 | ||
![]() |
4752fa6dd2 | ||
![]() |
4e08cf3879 | ||
![]() |
11bda8e76a | ||
![]() |
e33172060f | ||
![]() |
0dee6a9888 | ||
![]() |
3d855dcbfc | ||
![]() |
ed3b7f7971 | ||
![]() |
c0a4fb16e2 | ||
![]() |
688e7fff9c | ||
![]() |
880e56e563 | ||
![]() |
bf26d4ec95 | ||
![]() |
d5df3de1d7 | ||
![]() |
5d005211d4 | ||
![]() |
cc08ac9236 | ||
![]() |
09a6a340c9 | ||
![]() |
2692104ccd | ||
![]() |
b1fd539d25 | ||
![]() |
33d48fe4f7 | ||
![]() |
1ebc8e9362 | ||
![]() |
8b5a5241f2 | ||
![]() |
959b9ffbac | ||
![]() |
c9cbe41f9e | ||
![]() |
d62cbdc391 | ||
![]() |
31bd8ce7ae | ||
![]() |
c25db503e0 | ||
![]() |
ac51e6aae3 | ||
![]() |
72c9d616fd | ||
![]() |
52f80293a2 | ||
![]() |
a3a9edd41a | ||
![]() |
f9cbff1eec | ||
![]() |
b71482284f | ||
![]() |
d90e3a816e | ||
![]() |
869d875b5f | ||
![]() |
fb1a360360 | ||
![]() |
943765bd4d | ||
![]() |
9280113350 | ||
![]() |
627adb1755 | ||
![]() |
ad421eae11 | ||
![]() |
b0af9d535a | ||
![]() |
5ab69dfb7f | ||
![]() |
f1214e6ffd | ||
![]() |
a09e029216 | ||
![]() |
8782c70640 | ||
![]() |
7099a36bdb | ||
![]() |
7bd8b0c152 | ||
![]() |
b1cbf54711 | ||
![]() |
84f003d907 | ||
![]() |
4257e696da | ||
![]() |
c5c0d46ab8 | ||
![]() |
b397240664 | ||
![]() |
a31e27ee06 | ||
![]() |
483d7b6e58 | ||
![]() |
bfb6133871 | ||
![]() |
917ff89d9d | ||
![]() |
ef5762864f | ||
![]() |
50586cdb42 | ||
![]() |
82a0b8de0c | ||
![]() |
6db800d6d2 | ||
![]() |
b23bfc2f6e | ||
![]() |
929279b35a | ||
![]() |
795e4bad5f | ||
![]() |
6e20b6034e | ||
![]() |
6f34a27bd3 | ||
![]() |
240a605977 | ||
![]() |
4a6a471345 | ||
![]() |
f1be4f50a3 | ||
![]() |
8ef32e8081 | ||
![]() |
71e02ef833 | ||
![]() |
c70d7e475d | ||
![]() |
0f31f05e61 | ||
![]() |
7971a003cc | ||
![]() |
49491b9b8c | ||
![]() |
ea11f17954 | ||
![]() |
ebd37b9e2f | ||
![]() |
5de4fe3d23 | ||
![]() |
eb47f1227a | ||
![]() |
f84cde7d04 | ||
![]() |
4d3eb2887c | ||
![]() |
bc631612df | ||
![]() |
5d8a211961 | ||
![]() |
e62add6893 | ||
![]() |
44fc429f64 | ||
![]() |
ffe281f25d | ||
![]() |
ba39ff433d | ||
![]() |
795b8614ad | ||
![]() |
745a2f1886 | ||
![]() |
9bf58c89d4 | ||
![]() |
ee62b9a4c7 | ||
![]() |
e6125b893d | ||
![]() |
83ed333fa5 | ||
![]() |
39ed67d667 | ||
![]() |
ac2e973cb0 | ||
![]() |
94a0be3b05 | ||
![]() |
124e8af649 | ||
![]() |
f07ed68d82 | ||
![]() |
8f39dbf6b1 | ||
![]() |
6dde9ee6c4 | ||
![]() |
dbe6fe442d | ||
![]() |
1e2ac86ac6 | ||
![]() |
83c5ba04cd | ||
![]() |
b3f25c176b | ||
![]() |
52cf6375eb | ||
![]() |
82a3c37c16 | ||
![]() |
d63df5a156 | ||
![]() |
63ad3739fd | ||
![]() |
7eddb16f2f | ||
![]() |
7c4f4cacc9 | ||
![]() |
dc6ad72b2d | ||
![]() |
be729c87af | ||
![]() |
4ee83d9da4 | ||
![]() |
8b2f06442a | ||
![]() |
21181f011f | ||
![]() |
b4b099ecb1 | ||
![]() |
166b30bb0a | ||
![]() |
8eeb81f58e | ||
![]() |
0b20a1eeaa | ||
![]() |
d8cb8f7815 | ||
![]() |
36f79593cf | ||
![]() |
1014b25bf5 | ||
![]() |
55dcfc1a85 | ||
![]() |
9b6a628d51 | ||
![]() |
8b5a42073d | ||
![]() |
7991d40760 | ||
![]() |
4203296414 | ||
![]() |
93d319275f | ||
![]() |
94d262263c | ||
![]() |
ed90f21188 | ||
![]() |
80e0231727 | ||
![]() |
981197583a | ||
![]() |
6f58344e7b | ||
![]() |
07be844985 | ||
![]() |
45262583e6 | ||
![]() |
c113e38531 | ||
![]() |
8771f311d7 | ||
![]() |
fdec65e9bd | ||
![]() |
f8b46dc647 | ||
![]() |
847e47b5db | ||
![]() |
227bad9e99 | ||
![]() |
cb8168de41 | ||
![]() |
c200a0c7ac | ||
![]() |
81e80572d8 | ||
![]() |
2aa6c83714 | ||
![]() |
a22ea0b82b | ||
![]() |
af64579eb2 | ||
![]() |
f2705a611d | ||
![]() |
990dcc9d5a | ||
![]() |
c09237f0c3 | ||
![]() |
571a3533fb | ||
![]() |
6fcd9e1595 | ||
![]() |
9caa42d257 |
@@ -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
@@ -27,6 +27,7 @@ Makefile text
|
||||
*.yml text
|
||||
*.patch text
|
||||
*.txt text
|
||||
*.tpl text
|
||||
CODEOWNERS text
|
||||
*.plist text
|
||||
|
||||
|
7
.github/ISSUE_TEMPLATE.md
vendored
@@ -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. -->
|
||||
|
||||
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Alt+I` if you're on Mac OS. -->
|
||||
<!-- 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
@@ -47,3 +47,7 @@ node_modules
|
||||
# OSX files
|
||||
|
||||
.DS_Store
|
||||
|
||||
# VSCode files
|
||||
|
||||
.vscode
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"electron": {
|
||||
"npm_version": "6.7.0",
|
||||
"npm_version": "6.14.5",
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
@@ -9,45 +9,28 @@
|
||||
"libgtk-3-0",
|
||||
"libatk-bridge2.0-0",
|
||||
"libdbus-1-3",
|
||||
"libgbm1",
|
||||
"libc6"
|
||||
]
|
||||
},
|
||||
"builder": {
|
||||
"appId": "io.balena.etcher",
|
||||
"copyright": "Copyright 2016-2019 Balena Ltd",
|
||||
"copyright": "Copyright 2016-2021 Balena Ltd",
|
||||
"productName": "balenaEtcher",
|
||||
"nodeGypRebuild": true,
|
||||
"nodeGypRebuild": false,
|
||||
"afterPack": "./afterPack.js",
|
||||
"asar": false,
|
||||
"files": [
|
||||
"build/Release/elevator.node",
|
||||
"generated",
|
||||
"lib/shared/catalina-sudo/sudo-askpass.osascript.js",
|
||||
"lib/gui/app/index.html",
|
||||
"lib/gui/css/*.css",
|
||||
"lib/gui/css/fonts/*.woff2",
|
||||
"lib/gui/assets/*.svg",
|
||||
"assets/icon.png",
|
||||
"!node_modules/**/**",
|
||||
"node_modules/**/*.js",
|
||||
"node_modules/**/*.json",
|
||||
"node_modules/**/*.node",
|
||||
"node_modules/**/*.dll",
|
||||
"node_modules/node-raspberrypi-usbboot/blobs/**",
|
||||
"node_modules/flexboxgrid/dist/flexboxgrid.css",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff",
|
||||
"node_modules/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2"
|
||||
"lib/shared/catalina-sudo/sudo-askpass.osascript.js"
|
||||
],
|
||||
"afterSign": "./afterSignHook.js",
|
||||
"mac": {
|
||||
"asar": false,
|
||||
"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,
|
||||
@@ -74,10 +57,17 @@
|
||||
"synopsis": "balenaEtcher 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."
|
||||
},
|
||||
"deb": {
|
||||
"compression": "bzip2",
|
||||
"priority": "optional",
|
||||
"depends": [
|
||||
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
|
||||
]
|
||||
},
|
||||
"protocols": {
|
||||
"name": "etcher",
|
||||
"schemes": [
|
||||
"etcher"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
1094
CHANGELOG.md
@@ -1,2 +0,0 @@
|
||||
* @thundron @zvin @jviotti
|
||||
/scripts @nazrhom
|
4
FAQ.md
@@ -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).
|
||||
|
78
Makefile
@@ -3,18 +3,12 @@
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
RESIN_SCRIPTS ?= ./scripts/resin
|
||||
export NPM_VERSION ?= 6.7.0
|
||||
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,62 +117,20 @@ TARGETS = \
|
||||
help \
|
||||
info \
|
||||
lint \
|
||||
lint-js \
|
||||
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)
|
||||
|
||||
sass:
|
||||
npm rebuild node-sass
|
||||
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
lint-ts:
|
||||
resin-lint --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts
|
||||
|
||||
lint-sass:
|
||||
sass-lint lib/gui/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
|
||||
|
||||
# See https://github.com/electron/spectron/issues/127
|
||||
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
|
||||
test-spectron:
|
||||
ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" 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)"
|
||||
@@ -189,16 +140,11 @@ info:
|
||||
@echo "Host arch : $(HOST_ARCH)"
|
||||
@echo "Target arch : $(TARGET_ARCH)"
|
||||
|
||||
sanity-checks:
|
||||
./scripts/ci/ensure-staged-sass.sh
|
||||
./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)
|
||||
|
173
README.md
@@ -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).
|
||||
|
||||
[](https://balena.io/etcher)
|
||||
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||
[](https://david-dm.org/balena-io/etcher)
|
||||
[](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,66 +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
|
||||
|
||||
> [](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 keyserver.ubuntu.com --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
|
||||
```
|
||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
#### 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
|
||||
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
|
||||
```
|
||||
```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
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
###### Uninstall
|
||||
|
||||
```sh
|
||||
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 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
|
||||
sudo zypper rm balena-etcher-electron
|
||||
# remove the repo
|
||||
sudo zypper rr balena-etcher
|
||||
sudo zypper rr balena-etcher-source
|
||||
```
|
||||
|
||||
#### Solus (GNU/Linux x64)
|
||||
@@ -105,20 +156,34 @@ sudo eopkg it etcher
|
||||
sudo eopkg rm etcher
|
||||
```
|
||||
|
||||
#### Brew Cask (macOS)
|
||||
#### Arch/Manjaro Linux (GNU/Linux x64)
|
||||
|
||||
Note that the Etcher Cask has to be updated manually to point to new versions,
|
||||
so it might not refer to the latest version immediately after an Etcher
|
||||
release.
|
||||
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
|
||||
brew cask install balenaetcher
|
||||
yay -S balena-etcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
brew cask uninstall balenaetcher
|
||||
yay -R balena-etcher
|
||||
```
|
||||
|
||||
#### Brew (macOS)
|
||||
|
||||
**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 install balenaetcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
brew uninstall balenaetcher
|
||||
```
|
||||
|
||||
#### Chocolatey (Windows)
|
||||
@@ -138,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
|
||||
|
17
SUPPORT.md
@@ -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
@@ -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
|
10
afterPack.js
@@ -14,12 +14,16 @@ exports.default = function(context) {
|
||||
cp.execFileSync('mv', [scriptPath, binPath])
|
||||
fs.writeFileSync(
|
||||
scriptPath,
|
||||
outdent`
|
||||
outdent({trimTrailingNewline: false})`
|
||||
#!/bin/bash
|
||||
|
||||
# Resolve symlinks. Warning, readlink -f doesn't work on MacOS/BSD
|
||||
script_dir="$(dirname "$(readlink -f "\${BASH_SOURCE[0]}")")"
|
||||
|
||||
if [[ $EUID -ne 0 ]] || [[ $ELECTRON_RUN_AS_NODE ]]; then
|
||||
"\${BASH_SOURCE%/*}"/${context.packager.executableName}.bin "$@"
|
||||
"\${script_dir}"/${context.packager.executableName}.bin "$@"
|
||||
else
|
||||
"\${BASH_SOURCE%/*}"/${context.packager.executableName}.bin "$@" --no-sandbox
|
||||
"\${script_dir}"/${context.packager.executableName}.bin "$@" --no-sandbox
|
||||
fi
|
||||
`
|
||||
)
|
||||
|
@@ -1,10 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
const { notarize } = require('electron-notarize')
|
||||
const { ELECTRON_SKIP_NOTARIZATION } = process.env
|
||||
|
||||
async function main(context) {
|
||||
const { electronPlatformName, appOutDir } = context
|
||||
if (electronPlatformName !== 'darwin') {
|
||||
if (electronPlatformName !== 'darwin' || ELECTRON_SKIP_NOTARIZATION === 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
|
BIN
assets/icon.icns
35
binding.gyp
@@ -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++"
|
||||
]
|
||||
}
|
||||
} ]
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
@@ -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
|
||||
|
@@ -91,7 +91,7 @@ make electron-develop
|
||||
|
||||
```sh
|
||||
# Build the GUI
|
||||
make webpack
|
||||
npm run webpack
|
||||
# Start Electron
|
||||
npm start
|
||||
```
|
||||
|
@@ -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
|
||||
|
||||
@@ -166,7 +178,7 @@ Run the following command in `Terminal.app`, replacing `N` by the corresponding
|
||||
disk number, which you can find by running `diskutil list`:
|
||||
|
||||
```sh
|
||||
diskutil eraseDisk free UNTITLED /dev/diskN
|
||||
diskutil eraseDisk FAT32 UNTITLED MBRFormat /dev/diskN
|
||||
```
|
||||
|
||||
### GNU/Linux
|
||||
|
178
docs/docs/getting-started.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
description: Getting started for etcherPro
|
||||
slug: /
|
||||
---
|
||||
|
||||
# EtcherPro User manual
|
||||
|
||||
## 1. Features
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
| 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
|
||||
EtcherPro’s 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 don’t 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 Balena’s sole liability and Customer’s 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 Balena’s 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 Balena’s 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, purchaser’s 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 Balena’s 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 customer’s expense. Failure to obtain written confirmation within thirty (30) days of notification will result in the product being returned as is, at the customer’s 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.
|
BIN
docs/static/img/etcher-pro-rear-view.jpeg
vendored
Normal file
After Width: | Height: | Size: 195 KiB |
BIN
docs/static/img/etcher-pro-slots.jpeg
vendored
Normal file
After Width: | Height: | Size: 272 KiB |
BIN
docs/static/img/etcher-pro-top-view.jpeg
vendored
Normal file
After Width: | Height: | Size: 411 KiB |
BIN
docs/static/img/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/logo.png
vendored
Executable file
After Width: | Height: | Size: 5.4 KiB |
@@ -1,39 +1,21 @@
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2019 Balena Ltd
|
||||
copyright: Copyright 2016-2021 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: true
|
||||
nodeGypRebuild: false
|
||||
publish: null
|
||||
afterPack: "./afterPack.js"
|
||||
asar: false
|
||||
files:
|
||||
- build/Release/elevator.node
|
||||
- generated
|
||||
- lib/shared/catalina-sudo/sudo-askpass.osascript.js
|
||||
- lib/gui/app/index.html
|
||||
- lib/gui/css/*.css
|
||||
- lib/gui/css/fonts/*.woff2
|
||||
- lib/gui/assets/*.svg
|
||||
- assets/icon.png
|
||||
- "!node_modules/**/**"
|
||||
- "node_modules/**/*.js"
|
||||
- "node_modules/**/*.json"
|
||||
- "node_modules/**/*.node"
|
||||
- "node_modules/**/*.dll"
|
||||
- node_modules/node-raspberrypi-usbboot/blobs/**
|
||||
- node_modules/flexboxgrid/dist/flexboxgrid.css
|
||||
- node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff
|
||||
- node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff
|
||||
- node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff
|
||||
- node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff
|
||||
- node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff
|
||||
- node_modules/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2
|
||||
mac:
|
||||
asar: false
|
||||
icon: assets/icon.icns
|
||||
category: public.app-category.developer-tools
|
||||
hardenedRuntime: true
|
||||
entitlements: "entitlements.mac.plist"
|
||||
entitlementsInherit: "entitlements.mac.plist"
|
||||
artifactName: "${productName}-${version}.${ext}"
|
||||
dmg:
|
||||
background: assets/dmg/background.tiff
|
||||
icon: assets/icon.icns
|
||||
@@ -72,7 +54,6 @@ deb:
|
||||
depends:
|
||||
- gconf2
|
||||
- gconf-service
|
||||
- libappindicator1
|
||||
- libasound2
|
||||
- libatk1.0-0
|
||||
- libc6
|
||||
@@ -82,6 +63,7 @@ deb:
|
||||
- libexpat1
|
||||
- libfontconfig1
|
||||
- libfreetype6
|
||||
- libgbm1
|
||||
- libgcc1
|
||||
- libgconf-2-4
|
||||
- libgdk-pixbuf2.0-0
|
||||
@@ -91,7 +73,7 @@ deb:
|
||||
- libnotify4
|
||||
- libnspr4
|
||||
- libnss3
|
||||
- libpango1.0-0
|
||||
- libpango1.0-0 | libpango-1.0-0
|
||||
- libstdc++6
|
||||
- libx11-6
|
||||
- libxcomposite1
|
||||
@@ -105,7 +87,11 @@ deb:
|
||||
- libxss1
|
||||
- libxtst6
|
||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||
afterInstall: "./after-install.tpl"
|
||||
rpm:
|
||||
depends:
|
||||
- lsb
|
||||
- libXScrnSaver
|
||||
- util-linux
|
||||
protocols:
|
||||
name: etcher
|
||||
schemes:
|
||||
- etcher
|
||||
|
@@ -20,31 +20,31 @@ import * as _ from 'lodash';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import * as uuidV4 from 'uuid/v4';
|
||||
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 { 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';
|
||||
import { scanner as driveScanner } from './modules/drive-scanner';
|
||||
import * as exceptionReporter from './modules/exception-reporter';
|
||||
import { updateLock } from './modules/update-lock';
|
||||
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();
|
||||
},
|
||||
@@ -85,33 +85,45 @@ const currentVersion = packageJSON.version;
|
||||
analytics.logEvent('Application start', {
|
||||
packageType: packageJSON.packageType,
|
||||
version: currentVersion,
|
||||
applicationSessionUuid,
|
||||
});
|
||||
|
||||
const debouncedLog = _.debounce(console.log, 1000, { maxWait: 1000 });
|
||||
|
||||
function pluralize(word: string, quantity: number) {
|
||||
return `${quantity} ${word}${quantity === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
observe(() => {
|
||||
if (!flashState.isFlashing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFlashState = flashState.getFlashState();
|
||||
const stateType =
|
||||
!currentFlashState.flashing && currentFlashState.verifying
|
||||
? `Verifying ${currentFlashState.verifying}`
|
||||
: `Flashing ${currentFlashState.flashing}`;
|
||||
windowProgress.set(currentFlashState);
|
||||
|
||||
let eta = '';
|
||||
if (currentFlashState.eta !== undefined) {
|
||||
eta = `eta in ${currentFlashState.eta.toFixed(0)}s`;
|
||||
}
|
||||
let active = '';
|
||||
if (currentFlashState.type !== 'decompressing') {
|
||||
active = pluralize('device', currentFlashState.active);
|
||||
}
|
||||
// NOTE: There is usually a short time period between the `isFlashing()`
|
||||
// property being set, and the flashing actually starting, which
|
||||
// might cause some non-sense flashing state logs including
|
||||
// `undefined` values.
|
||||
analytics.logDebug(
|
||||
`${stateType} devices, ` +
|
||||
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` +
|
||||
`(total ${currentFlashState.totalSpeed} MB/s) ` +
|
||||
`eta in ${currentFlashState.eta}s ` +
|
||||
`with ${currentFlashState.failed} failed devices`,
|
||||
);
|
||||
|
||||
windowProgress.set(currentFlashState);
|
||||
debouncedLog(outdent({ newline: ' ' })`
|
||||
${_.capitalize(currentFlashState.type)}
|
||||
${active},
|
||||
${currentFlashState.percentage}%
|
||||
at
|
||||
${(currentFlashState.speed || 0).toFixed(2)}
|
||||
MB/s
|
||||
(total ${(currentFlashState.speed * currentFlashState.active).toFixed(2)} MB/s)
|
||||
${eta}
|
||||
with
|
||||
${pluralize('failed device', currentFlashState.failed)}
|
||||
`);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -153,19 +165,16 @@ const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary<string> = {
|
||||
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3',
|
||||
};
|
||||
|
||||
const BLACKLISTED_DRIVES = settings.has('driveBlacklist')
|
||||
? settings.get('driveBlacklist').split(',')
|
||||
: [];
|
||||
|
||||
function driveIsAllowed(drive: {
|
||||
async function driveIsAllowed(drive: {
|
||||
devicePath: string;
|
||||
device: string;
|
||||
raw: string;
|
||||
}) {
|
||||
const driveBlacklist = (await settings.get('driveBlacklist')) || [];
|
||||
return !(
|
||||
BLACKLISTED_DRIVES.includes(drive.devicePath) ||
|
||||
BLACKLISTED_DRIVES.includes(drive.device) ||
|
||||
BLACKLISTED_DRIVES.includes(drive.raw)
|
||||
driveBlacklist.includes(drive.devicePath) ||
|
||||
driveBlacklist.includes(drive.device) ||
|
||||
driveBlacklist.includes(drive.raw)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,7 +195,7 @@ function prepareDrive(drive: Drive) {
|
||||
// @ts-ignore
|
||||
drive.progress = 0;
|
||||
drive.disabled = true;
|
||||
drive.on('progress', progress => {
|
||||
drive.on('progress', (progress) => {
|
||||
updateDriveProgress(drive, progress);
|
||||
});
|
||||
return drive;
|
||||
@@ -207,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`
|
||||
@@ -222,17 +230,17 @@ 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');
|
||||
}
|
||||
|
||||
function addDrive(drive: Drive) {
|
||||
async function addDrive(drive: Drive) {
|
||||
const preparedDrive = prepareDrive(drive);
|
||||
if (!driveIsAllowed(preparedDrive)) {
|
||||
if (!(await driveIsAllowed(preparedDrive))) {
|
||||
return;
|
||||
}
|
||||
const drives = getDrives();
|
||||
@@ -241,6 +249,15 @@ 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];
|
||||
@@ -255,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);
|
||||
}
|
||||
}
|
||||
@@ -263,7 +281,7 @@ function updateDriveProgress(
|
||||
driveScanner.on('attach', addDrive);
|
||||
driveScanner.on('detach', removeDrive);
|
||||
|
||||
driveScanner.on('error', error => {
|
||||
driveScanner.on('error', (error) => {
|
||||
// Stop the drive scanning loop in case of errors,
|
||||
// otherwise we risk presenting the same error over
|
||||
// and over again to the user, while also heavily
|
||||
@@ -277,11 +295,10 @@ driveScanner.start();
|
||||
|
||||
let popupExists = false;
|
||||
|
||||
window.addEventListener('beforeunload', async event => {
|
||||
window.addEventListener('beforeunload', async (event) => {
|
||||
if (!flashState.isFlashing() || popupExists) {
|
||||
analytics.logEvent('Close application', {
|
||||
isFlashing: flashState.isFlashing(),
|
||||
applicationSessionUuid,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -292,10 +309,7 @@ window.addEventListener('beforeunload', async event => {
|
||||
// Don't open any more popups
|
||||
popupExists = true;
|
||||
|
||||
analytics.logEvent('Close attempt while flashing', {
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
analytics.logEvent('Close attempt while flashing');
|
||||
|
||||
try {
|
||||
const confirmed = await osDialog.showWarning({
|
||||
@@ -307,8 +321,6 @@ window.addEventListener('beforeunload', async event => {
|
||||
if (confirmed) {
|
||||
analytics.logEvent('Close confirmed while flashing', {
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
|
||||
// This circumvents the 'beforeunload' event unlike
|
||||
@@ -321,21 +333,31 @@ window.addEventListener('beforeunload', async event => {
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
popupExists = false;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
});
|
||||
|
||||
function extendLock() {
|
||||
updateLock.extend();
|
||||
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
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener('click', extendLock);
|
||||
window.addEventListener('touchstart', extendLock);
|
||||
|
||||
// Initial update lock acquisition
|
||||
extendLock();
|
||||
|
||||
settings.load().catch(exceptionReporter.report);
|
||||
|
||||
ReactDOM.render(React.createElement(MainPage), document.getElementById('main'));
|
||||
|
@@ -1,292 +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),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
|
||||
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,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
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)', {
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
const hasStatus = hasListDriveImageCompatibilityStatus(
|
||||
selectionState.getSelectedDrives(),
|
||||
selectionState.getImage(),
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="modal-drive-selector-modal"
|
||||
title="Select a Drive"
|
||||
done={close}
|
||||
action="Continue"
|
||||
style={{
|
||||
padding: '20px 30px 11px 30px',
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
primary: !hasStatus,
|
||||
warning: hasStatus,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<ul
|
||||
style={{
|
||||
height: '250px',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
padding: '0',
|
||||
}}
|
||||
>
|
||||
{_.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={`../assets/${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>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
556
lib/gui/app/components/drive-selector/drive-selector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
@@ -1,57 +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 {
|
||||
await settings.load();
|
||||
const endpoint =
|
||||
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;
|
||||
}
|
||||
}
|
@@ -14,121 +14,110 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as uuidV4 from 'uuid/v4';
|
||||
import { Flex } from 'rendition';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import { store } from '../../models/store';
|
||||
import * as settings from '../../models/settings';
|
||||
import { Actions, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { updateLock } from '../../modules/update-lock';
|
||||
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';
|
||||
|
||||
const restart = (options: any, goToMain: () => void) => {
|
||||
const {
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
} = store.getState().toJS();
|
||||
if (!options.preserveImage) {
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
function restart(goToMain: () => void) {
|
||||
selectionState.deselectAllDrives();
|
||||
analytics.logEvent('Restart', {
|
||||
...options,
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
|
||||
// Re-enable lock release on inactivity
|
||||
updateLock.resume();
|
||||
analytics.logEvent('Restart');
|
||||
|
||||
// Reset the flashing workflow uuid
|
||||
store.dispatch({
|
||||
type: 'SET_FLASHING_WORKFLOW_UUID',
|
||||
type: Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||
data: uuidV4(),
|
||||
});
|
||||
|
||||
goToMain();
|
||||
};
|
||||
}
|
||||
|
||||
const 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 }) {
|
||||
// @ts-ignore
|
||||
const results = flashState.getFlashResults().results || {};
|
||||
const progressMessage = messages.progress;
|
||||
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}
|
||||
message={progressMessage}
|
||||
errors={formattedErrors}
|
||||
></FlashResults>
|
||||
<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={(options: any) => restart(options, goToMain)}
|
||||
></FlashAnother>
|
||||
</div>
|
||||
|
||||
<div className="box center">
|
||||
<div className="fallback-banner">
|
||||
<div className="caption caption-big">
|
||||
Thanks for using
|
||||
<span
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://balena.io/etcher?ref=etcher_offline_banner',
|
||||
)
|
||||
}
|
||||
>
|
||||
<SVGIcon
|
||||
paths={['../../assets/etcher.svg']}
|
||||
width="165px"
|
||||
height="auto"
|
||||
></SVGIcon>
|
||||
</span>
|
||||
</div>
|
||||
<div className="caption caption-small fallback-footer">
|
||||
made with
|
||||
<SVGIcon
|
||||
paths={['../../assets/love.svg']}
|
||||
width="auto"
|
||||
height="20px"
|
||||
></SVGIcon>
|
||||
by
|
||||
<span
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
openExternal('https://balena.io?ref=etcher_success')
|
||||
}
|
||||
>
|
||||
<SVGIcon
|
||||
paths={['../../assets/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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -15,30 +15,17 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { position, right } from 'styled-system';
|
||||
import { BaseButton, ThemedProvider } from '../../styled-components';
|
||||
|
||||
const Div = styled.div<any>`
|
||||
${position}
|
||||
${right}
|
||||
`;
|
||||
import { BaseButton } from '../../styled-components';
|
||||
|
||||
export interface FlashAnotherProps {
|
||||
onClick: (options: { preserveImage: boolean }) => void;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<Div position="absolute" right="152px">
|
||||
<BaseButton
|
||||
primary
|
||||
onClick={props.onClick.bind(null, { preserveImage: true })}
|
||||
>
|
||||
Flash Another
|
||||
</BaseButton>
|
||||
</Div>
|
||||
</ThemedProvider>
|
||||
<BaseButton primary onClick={props.onClick}>
|
||||
Flash another
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
|
@@ -14,52 +14,230 @@
|
||||
* 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 { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
import { left, position, space, top } from 'styled-system';
|
||||
import { Underline } from '../../styled-components';
|
||||
|
||||
const Div: any = styled.div<any>`
|
||||
${position}
|
||||
${top}
|
||||
${left}
|
||||
${space}
|
||||
import { progress } from '../../../../shared/messages';
|
||||
import { bytesToMegabytes } from '../../../../shared/units';
|
||||
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const FlashResults: any = ({
|
||||
errors,
|
||||
results,
|
||||
message,
|
||||
}: {
|
||||
errors: () => string;
|
||||
results: any;
|
||||
message: any;
|
||||
const DoneIcon = (props: {
|
||||
skipped: boolean;
|
||||
color: string;
|
||||
allFailed: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Div position="absolute" left="153px" top="66px">
|
||||
<div className="inline-flex title">
|
||||
<span className="tick tick--success space-right-medium"></span>
|
||||
<h3>Flash Complete!</h3>
|
||||
</div>
|
||||
<Div className="results" mt="11px" mr="0" mb="0" ml="40px">
|
||||
<Underline tooltip={errors()}>
|
||||
{_.map(results.devices, (quantity, type) => {
|
||||
return quantity ? (
|
||||
<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">
|
||||
{message[type](quantity)}
|
||||
</span>
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
</Underline>
|
||||
</Div>
|
||||
</Div>
|
||||
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
|
||||
}: {
|
||||
goToMain: () => void;
|
||||
image?: string;
|
||||
errors: FlashError[];
|
||||
skip: boolean;
|
||||
results: {
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
devices: { failed: number; successful: number };
|
||||
};
|
||||
} & 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 (
|
||||
<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
|
||||
fontSize="10px"
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
tooltip={outdent({ newline: ' ' })`
|
||||
The speed is calculated by dividing the image size by the flashing time.
|
||||
Disk images with ext partitions flash faster as we are able to skip unused parts.
|
||||
`}
|
||||
>
|
||||
Effective speed: {effectiveSpeed} MB/s
|
||||
</Txt>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@@ -1,413 +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 sdk from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import { default as Dropzone } from 'react-dropzone';
|
||||
import { Modal } from 'rendition';
|
||||
import { default as styled } from 'styled-components';
|
||||
|
||||
import * as errors from '../../../../shared/errors';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import * as supportedFormats from '../../../../shared/supported-formats';
|
||||
import * as shared from '../../../../shared/units';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import { observe, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import * as exceptionReporter from '../../modules/exception-reporter';
|
||||
import * as osDialog from '../../os/dialog';
|
||||
import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives';
|
||||
import {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
Footer,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
StepSelection,
|
||||
Underline,
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
|
||||
// TODO move these styles to rendition
|
||||
const ModalText = styled.p`
|
||||
a {
|
||||
color: rgb(0, 174, 239);
|
||||
|
||||
&:hover {
|
||||
color: rgb(0, 139, 191);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const mainSupportedExtensions = _.intersection(
|
||||
['img', 'iso', 'zip'],
|
||||
supportedFormats.getAllExtensions(),
|
||||
);
|
||||
|
||||
const extraSupportedExtensions = _.difference(
|
||||
supportedFormats.getAllExtensions(),
|
||||
mainSupportedExtensions,
|
||||
).sort();
|
||||
|
||||
function getState() {
|
||||
return {
|
||||
hasImage: selectionState.hasImage(),
|
||||
imageName: selectionState.getImageName(),
|
||||
imageSize: selectionState.getImageSize(),
|
||||
};
|
||||
}
|
||||
|
||||
interface ImageSelectorProps {
|
||||
flashing: boolean;
|
||||
}
|
||||
|
||||
interface ImageSelectorState {
|
||||
hasImage: boolean;
|
||||
imageName: string;
|
||||
imageSize: number;
|
||||
warning: { message: string; title: string | null } | null;
|
||||
showImageDetails: boolean;
|
||||
}
|
||||
|
||||
export class ImageSelector extends React.Component<
|
||||
ImageSelectorProps,
|
||||
ImageSelectorState
|
||||
> {
|
||||
private unsubscribe: () => void;
|
||||
|
||||
constructor(props: ImageSelectorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...getState(),
|
||||
warning: null,
|
||||
showImageDetails: false,
|
||||
};
|
||||
|
||||
this.openImageSelector = this.openImageSelector.bind(this);
|
||||
this.reselectImage = this.reselectImage.bind(this);
|
||||
this.handleOnDrop = this.handleOnDrop.bind(this);
|
||||
this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.unsubscribe = observe(() => {
|
||||
this.setState(getState());
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
private reselectImage() {
|
||||
analytics.logEvent('Reselect image', {
|
||||
previousImage: selectionState.getImage(),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
|
||||
this.openImageSelector();
|
||||
}
|
||||
|
||||
private selectImage(
|
||||
image: sdk.sourceDestination.Metadata & {
|
||||
path: string;
|
||||
extension: string;
|
||||
hasMBR: boolean;
|
||||
},
|
||||
) {
|
||||
if (!supportedFormats.isSupportedImage(image.path)) {
|
||||
const invalidImageError = errors.createUserError({
|
||||
title: 'Invalid image',
|
||||
description: messages.error.invalidImage(image.path),
|
||||
});
|
||||
|
||||
osDialog.showError(invalidImageError);
|
||||
analytics.logEvent(
|
||||
'Invalid image',
|
||||
_.merge(
|
||||
{
|
||||
applicationSessionUuid: store.getState().toJS()
|
||||
.applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
},
|
||||
image,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let message = null;
|
||||
let title = null;
|
||||
|
||||
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
||||
analytics.logEvent('Possibly Windows image', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS()
|
||||
.applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
message = messages.warning.looksLikeWindowsImage();
|
||||
title = 'Possible Windows image detected';
|
||||
} else if (!image.hasMBR) {
|
||||
analytics.logEvent('Missing partition table', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS()
|
||||
.applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
title = 'Missing partition table';
|
||||
message = messages.warning.missingPartitionTable();
|
||||
}
|
||||
|
||||
if (message) {
|
||||
this.setState({
|
||||
warning: {
|
||||
message,
|
||||
title,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
selectionState.selectImage(image);
|
||||
analytics.logEvent('Select image', {
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
// certain features without printing pages of text to DevTools.
|
||||
image: {
|
||||
...image,
|
||||
logo: Boolean(image.logo),
|
||||
blockMap: Boolean(image.blockMap),
|
||||
},
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
} catch (error) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async selectImageByPath(imagePath: string) {
|
||||
try {
|
||||
imagePath = await replaceWindowsNetworkDriveLetter(imagePath);
|
||||
} catch (error) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
if (!supportedFormats.isSupportedImage(imagePath)) {
|
||||
const invalidImageError = errors.createUserError({
|
||||
title: 'Invalid image',
|
||||
description: messages.error.invalidImage(imagePath),
|
||||
});
|
||||
|
||||
osDialog.showError(invalidImageError);
|
||||
analytics.logEvent('Invalid image', { path: imagePath });
|
||||
return;
|
||||
}
|
||||
|
||||
const source = new sdk.sourceDestination.File(
|
||||
imagePath,
|
||||
sdk.sourceDestination.File.OpenFlags.Read,
|
||||
);
|
||||
try {
|
||||
const innerSource = await source.getInnerSource();
|
||||
const metadata = (await innerSource.getMetadata()) as sdk.sourceDestination.Metadata & {
|
||||
hasMBR: boolean;
|
||||
partitions: MBRPartition[] | GPTPartition[];
|
||||
path: string;
|
||||
extension: string;
|
||||
};
|
||||
const partitionTable = await innerSource.getPartitionTable();
|
||||
if (partitionTable) {
|
||||
metadata.hasMBR = true;
|
||||
metadata.partitions = partitionTable.partitions;
|
||||
} else {
|
||||
metadata.hasMBR = false;
|
||||
}
|
||||
metadata.path = imagePath;
|
||||
metadata.extension = path.extname(imagePath).slice(1);
|
||||
this.selectImage(metadata);
|
||||
} catch (error) {
|
||||
const imageError = errors.createUserError({
|
||||
title: 'Error opening image',
|
||||
description: messages.error.openImage(
|
||||
path.basename(imagePath),
|
||||
error.message,
|
||||
),
|
||||
});
|
||||
osDialog.showError(imageError);
|
||||
analytics.logException(error);
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
} catch (error) {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async openImageSelector() {
|
||||
analytics.logEvent('Open image selector', {
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
|
||||
try {
|
||||
const imagePath = await osDialog.selectImage();
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (!imagePath) {
|
||||
analytics.logEvent('Image selector closed', {
|
||||
applicationSessionUuid: store.getState().toJS()
|
||||
.applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.selectImageByPath(imagePath);
|
||||
} catch (error) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleOnDrop(acceptedFiles: Array<{ path: string }>) {
|
||||
const [file] = acceptedFiles;
|
||||
|
||||
if (file) {
|
||||
this.selectImageByPath(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
private showSelectedImageDetails() {
|
||||
analytics.logEvent('Show selected image tooltip', {
|
||||
imagePath: selectionState.getImagePath(),
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
showImageDetails: true,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO add a visual change when dragging a file over the selector
|
||||
public render() {
|
||||
const { flashing } = this.props;
|
||||
const { showImageDetails } = this.state;
|
||||
|
||||
const hasImage = selectionState.hasImage();
|
||||
|
||||
const imageBasename = hasImage
|
||||
? path.basename(selectionState.getImagePath())
|
||||
: '';
|
||||
const imageName = selectionState.getImageName();
|
||||
const imageSize = selectionState.getImageSize();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="box text-center relative">
|
||||
<Dropzone multiple={false} onDrop={this.handleOnDrop}>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div className="center-block" {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
<SVGIcon
|
||||
contents={[selectionState.getImageLogo()]}
|
||||
paths={['../../assets/image.svg']}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
|
||||
<div className="space-vertical-large">
|
||||
{hasImage ? (
|
||||
<>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={this.showSelectedImageDetails}
|
||||
tooltip={imageBasename}
|
||||
>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
</StepNameButton>
|
||||
{!flashing && (
|
||||
<ChangeButton plain mb={14} onClick={this.reselectImage}>
|
||||
Change
|
||||
</ChangeButton>
|
||||
)}
|
||||
<DetailsText>
|
||||
{shared.bytesToClosestUnit(imageSize)}
|
||||
</DetailsText>
|
||||
</>
|
||||
) : (
|
||||
<StepSelection>
|
||||
<StepButton onClick={this.openImageSelector}>
|
||||
Select image
|
||||
</StepButton>
|
||||
<Footer>
|
||||
{mainSupportedExtensions.join(', ')}, and{' '}
|
||||
<Underline tooltip={extraSupportedExtensions.join(', ')}>
|
||||
many more
|
||||
</Underline>
|
||||
</Footer>
|
||||
</StepSelection>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.warning != null && (
|
||||
<Modal
|
||||
titleElement={
|
||||
<span>
|
||||
<span
|
||||
style={{ color: '#d9534f' }}
|
||||
className="glyphicon glyphicon-exclamation-sign"
|
||||
></span>{' '}
|
||||
<span>{this.state.warning.title}</span>
|
||||
</span>
|
||||
}
|
||||
action="Continue"
|
||||
cancel={() => {
|
||||
this.setState({ warning: null });
|
||||
this.reselectImage();
|
||||
}}
|
||||
done={() => {
|
||||
this.setState({ warning: null });
|
||||
}}
|
||||
primaryButtonProps={{ warning: true, primary: false }}
|
||||
>
|
||||
<ModalText
|
||||
dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{showImageDetails && (
|
||||
<Modal
|
||||
title="Image File Name"
|
||||
done={() => {
|
||||
this.setState({ showImageDetails: false });
|
||||
}}
|
||||
>
|
||||
{selectionState.getImagePath()}
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@@ -14,132 +14,120 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Color from 'color';
|
||||
import * as React from 'react';
|
||||
import { ProgressBar } from 'rendition';
|
||||
import { css, default as styled, keyframes } from 'styled-components';
|
||||
import { Flex, Button, ProgressBar, Txt } from 'rendition';
|
||||
import { default as styled } from 'styled-components';
|
||||
|
||||
import { StepButton, StepSelection } from '../../styled-components';
|
||||
import { colors } from '../../theme';
|
||||
|
||||
const darkenForegroundStripes = 0.18;
|
||||
const desaturateForegroundStripes = 0.2;
|
||||
const progressButtonStripesForegroundColor = Color(colors.primary.background)
|
||||
.darken(darkenForegroundStripes)
|
||||
.desaturate(desaturateForegroundStripes)
|
||||
.string();
|
||||
|
||||
const desaturateBackgroundStripes = 0.05;
|
||||
const progressButtonStripesBackgroundColor = Color(colors.primary.background)
|
||||
.desaturate(desaturateBackgroundStripes)
|
||||
.string();
|
||||
|
||||
const ProgressButtonStripes = keyframes`
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 20px 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ProgressButtonStripesRule = css`
|
||||
${ProgressButtonStripes} 1s linear infinite;
|
||||
`;
|
||||
import { fromFlashState } from '../../modules/progress-status';
|
||||
import { StepButton } from '../../styled-components';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
> div {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
transition-duration: 0s;
|
||||
> div {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 14px;
|
||||
font-size: 16px;
|
||||
line-height: 48px;
|
||||
|
||||
background: ${Color(colors.warning.background)
|
||||
.darken(darkenForegroundStripes)
|
||||
.string()};
|
||||
`;
|
||||
|
||||
const FlashProgressBarValidating = styled(FlashProgressBar)`
|
||||
// Notice that we add 0.01 to certain gradient stop positions.
|
||||
// That workarounds a Chrome rendering issue where diagonal
|
||||
// lines look spiky.
|
||||
// See https://github.com/balena-io/etcher/issues/472
|
||||
|
||||
background-image: -webkit-gradient(
|
||||
linear,
|
||||
0 0,
|
||||
100% 100%,
|
||||
color-stop(0.25, ${progressButtonStripesForegroundColor}),
|
||||
color-stop(0.26, ${progressButtonStripesBackgroundColor}),
|
||||
color-stop(0.5, ${progressButtonStripesBackgroundColor}),
|
||||
color-stop(0.51, ${progressButtonStripesForegroundColor}),
|
||||
color-stop(0.75, ${progressButtonStripesForegroundColor}),
|
||||
color-stop(0.76, ${progressButtonStripesBackgroundColor}),
|
||||
to(${progressButtonStripesBackgroundColor})
|
||||
);
|
||||
|
||||
background-color: white;
|
||||
|
||||
animation: ${ProgressButtonStripesRule};
|
||||
overflow: hidden;
|
||||
|
||||
background-size: 20px 20px;
|
||||
background: #2f3033;
|
||||
`;
|
||||
|
||||
interface ProgressButtonProps {
|
||||
striped: boolean;
|
||||
type: 'decompressing' | 'flashing' | 'verifying';
|
||||
active: boolean;
|
||||
percentage: number;
|
||||
label: string;
|
||||
position: number;
|
||||
disabled: boolean;
|
||||
callback: () => any;
|
||||
cancel: (type: string) => void;
|
||||
callback: () => void;
|
||||
warning?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress Button component
|
||||
*/
|
||||
export class ProgressButton extends React.Component<ProgressButtonProps> {
|
||||
public render() {
|
||||
if (this.props.active) {
|
||||
if (this.props.striped) {
|
||||
return (
|
||||
<StepSelection>
|
||||
<FlashProgressBarValidating
|
||||
primary
|
||||
emphasized
|
||||
value={this.props.percentage}
|
||||
>
|
||||
{this.props.label}
|
||||
</FlashProgressBarValidating>
|
||||
</StepSelection>
|
||||
);
|
||||
}
|
||||
const colors = {
|
||||
decompressing: '#00aeef',
|
||||
flashing: '#da60ff',
|
||||
verifying: '#1ac135',
|
||||
} as const;
|
||||
|
||||
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;
|
||||
height: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
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,
|
||||
});
|
||||
const type = this.props.type || 'default';
|
||||
if (this.props.active) {
|
||||
return (
|
||||
<StepSelection>
|
||||
<FlashProgressBar warning emphasized value={this.props.percentage}>
|
||||
{this.props.label}
|
||||
</FlashProgressBar>
|
||||
</StepSelection>
|
||||
<>
|
||||
<Flex
|
||||
alignItems="baseline"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
style={{
|
||||
marginTop: 42,
|
||||
marginBottom: '6px',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Flex>
|
||||
<Txt color="#fff">{status} </Txt>
|
||||
<Txt color={colors[type]}>{position}</Txt>
|
||||
</Flex>
|
||||
{type && (
|
||||
<CancelButton
|
||||
type={type}
|
||||
onClick={this.props.cancel}
|
||||
color="#00aeef"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<FlashProgressBar background={colors[type]} value={percentage} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StepSelection>
|
||||
<StepButton
|
||||
onClick={this.props.callback}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{this.props.label}
|
||||
</StepButton>
|
||||
</StepSelection>
|
||||
<StepButton
|
||||
primary={!warning}
|
||||
warning={warning}
|
||||
onClick={this.props.callback}
|
||||
disabled={this.props.disabled}
|
||||
style={{
|
||||
marginTop: 30,
|
||||
}}
|
||||
>
|
||||
Flash!
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -15,81 +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={['../../assets/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={['../../assets/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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,6 @@ import * as React from 'react';
|
||||
|
||||
import * as packageJSON from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import { store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
|
||||
/**
|
||||
@@ -59,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 {
|
||||
@@ -92,7 +90,7 @@ export class SafeWebview extends React.PureComponent<
|
||||
url.searchParams.set(API_VERSION_PARAM, API_VERSION);
|
||||
url.searchParams.set(
|
||||
OPT_OUT_ANALYTICS_PARAM,
|
||||
(!settings.get('errorReporting')).toString(),
|
||||
(!settings.getSync('errorReporting')).toString(),
|
||||
);
|
||||
this.entryHref = url.href;
|
||||
// Events steal 'this'
|
||||
@@ -110,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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -182,11 +183,7 @@ export class SafeWebview extends React.PureComponent<
|
||||
// only care about this event if it's a request for the main frame
|
||||
if (event.resourceType === 'mainFrame') {
|
||||
const HTTP_OK = 200;
|
||||
analytics.logEvent('SafeWebview loaded', {
|
||||
event,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
analytics.logEvent('SafeWebview loaded', { event });
|
||||
this.setState({
|
||||
shouldShow: event.statusCode === HTTP_OK,
|
||||
});
|
||||
@@ -197,15 +194,13 @@ export class SafeWebview extends React.PureComponent<
|
||||
}
|
||||
|
||||
// Open link in browser if it's opened as a 'foreground-tab'
|
||||
public static newWindow(event: electron.NewWindowEvent) {
|
||||
public static async newWindow(event: electron.NewWindowEvent) {
|
||||
const url = new window.URL(event.url);
|
||||
if (
|
||||
_.every([
|
||||
url.protocol === 'http:' || url.protocol === 'https:',
|
||||
event.disposition === 'foreground-tab',
|
||||
// Don't open links if they're disabled by the env var
|
||||
!settings.get('disableExternalLinks'),
|
||||
])
|
||||
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
||||
event.disposition === 'foreground-tab' &&
|
||||
// Don't open links if they're disabled by the env var
|
||||
!(await settings.get('disableExternalLinks'))
|
||||
) {
|
||||
electron.shell.openExternal(url.href);
|
||||
}
|
||||
|
@@ -14,210 +14,132 @@
|
||||
* 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 styled from 'styled-components';
|
||||
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 { store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
|
||||
const { useState } = React;
|
||||
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?: any;
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
const settingsList: 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',
|
||||
},
|
||||
{
|
||||
name: 'trim',
|
||||
label: 'Trim ext{2,3,4} partitions before writing (raw images only)',
|
||||
},
|
||||
{
|
||||
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',
|
||||
async function getSettingsList(): Promise<Setting[]> {
|
||||
const list: Setting[] = [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||
},
|
||||
hide: settings.get('disableUnsafeMode'),
|
||||
},
|
||||
];
|
||||
];
|
||||
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||
list.push({
|
||||
name: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const SettingsModal: any = styled(
|
||||
({ toggleModal }: SettingsModalProps) => {
|
||||
const [currentSettings, setCurrentSettings]: [
|
||||
_.Dictionary<any>,
|
||||
React.Dispatch<React.SetStateAction<_.Dictionary<any>>>,
|
||||
] = useState(settings.getAll());
|
||||
const [warning, setWarning]: [
|
||||
any,
|
||||
React.Dispatch<React.SetStateAction<any>>,
|
||||
] = useState({});
|
||||
const UUID = process.env.BALENA_DEVICE_UUID;
|
||||
|
||||
const toggleSetting = async (setting: string, options?: any) => {
|
||||
const value = currentSettings[setting];
|
||||
const dangerous = !_.isUndefined(options);
|
||||
|
||||
analytics.logEvent('Toggle setting', {
|
||||
setting,
|
||||
value,
|
||||
dangerous,
|
||||
// @ts-ignore
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
});
|
||||
|
||||
if (value || !dangerous) {
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
setWarning({});
|
||||
return;
|
||||
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(() => {
|
||||
(async () => {
|
||||
if (settingsList.length === 0) {
|
||||
setCurrentSettingsList(await getSettingsList());
|
||||
}
|
||||
})();
|
||||
});
|
||||
const [currentSettings, setCurrentSettings] = React.useState<
|
||||
_.Dictionary<boolean>
|
||||
>({});
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (_.isEmpty(currentSettings)) {
|
||||
setCurrentSettings(await settings.getAll());
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
// Show warning since it's a dangerous setting
|
||||
setWarning({
|
||||
setting,
|
||||
settingValue: value,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
const toggleSetting = async (setting: string) => {
|
||||
const value = currentSettings[setting];
|
||||
analytics.logEvent('Toggle setting', { setting, value });
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
id="settings-modal"
|
||||
title="Settings"
|
||||
done={() => toggleModal(false)}
|
||||
style={{
|
||||
width: 780,
|
||||
height: 420,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{_.map(settingsList, (setting: Setting, i: number) => {
|
||||
return setting.hide ? null : (
|
||||
<div key={setting.name}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
label={setting.label}
|
||||
checked={currentSettings[setting.name]}
|
||||
onChange={() => toggleSetting(setting.name, setting.options)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div>
|
||||
<span
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} /> {version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{_.isEmpty(warning) ? null : (
|
||||
<WarningModal
|
||||
message={warning.description}
|
||||
confirmLabel={warning.confirmLabel}
|
||||
done={() => {
|
||||
settings.set(warning.setting, !warning.settingValue);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[warning.setting]: true,
|
||||
});
|
||||
setWarning({});
|
||||
}}
|
||||
cancel={() => {
|
||||
setWarning({});
|
||||
}}
|
||||
/>
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Txt fontSize={24} mb={24}>
|
||||
Settings
|
||||
</Txt>
|
||||
}
|
||||
done={() => toggleModal(false)}
|
||||
>
|
||||
<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)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
{UUID !== undefined && (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={24}>System Information</Txt>
|
||||
<InfoBox label="UUID" value={UUID.substr(0, 7)} />
|
||||
</Flex>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
)`
|
||||
> div:nth-child(3) {
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
<Flex
|
||||
mt={18}
|
||||
alignItems="center"
|
||||
color="#00aeef"
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
}}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
878
lib/gui/app/components/source-selector/source-selector.tsx
Normal file
@@ -0,0 +1,878 @@
|
||||
/*
|
||||
* 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 CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
|
||||
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
||||
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||
import * as path from 'path';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Flex,
|
||||
ButtonProps,
|
||||
Modal as SmallModal,
|
||||
Txt,
|
||||
Card as BaseCard,
|
||||
Input,
|
||||
Spinner,
|
||||
Link,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as errors from '../../../../shared/errors';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import * as supportedFormats from '../../../../shared/supported-formats';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import * as exceptionReporter from '../../modules/exception-reporter';
|
||||
import * as osDialog from '../../os/dialog';
|
||||
import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives';
|
||||
import {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
Modal,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
ScrollableFlex,
|
||||
} from '../../styled-components';
|
||||
import { colors } from '../../theme';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import SrcSvg from '../../../assets/src.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { isJson } from '../../../../shared/utils';
|
||||
|
||||
const recentUrlImagesKey = 'recentUrlImages';
|
||||
|
||||
function normalizeRecentUrlImages(urls: any[]): URL[] {
|
||||
if (!Array.isArray(urls)) {
|
||||
urls = [];
|
||||
}
|
||||
urls = urls
|
||||
.map((url) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (error: any) {
|
||||
// Invalid URL, skip
|
||||
}
|
||||
})
|
||||
.filter((url) => url !== undefined);
|
||||
urls = _.uniqBy(urls, (url) => url.href);
|
||||
return urls.slice(urls.length - 5);
|
||||
}
|
||||
|
||||
function getRecentUrlImages(): URL[] {
|
||||
let urls = [];
|
||||
try {
|
||||
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return normalizeRecentUrlImages(urls);
|
||||
}
|
||||
|
||||
function setRecentUrlImages(urls: URL[]) {
|
||||
const normalized = normalizeRecentUrlImages(urls.map((url: URL) => url.href));
|
||||
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
|
||||
}
|
||||
|
||||
const isURL = (imagePath: string) =>
|
||||
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||
|
||||
const Card = styled(BaseCard)`
|
||||
hr {
|
||||
margin: 5px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO move these styles to rendition
|
||||
const ModalText = styled.p`
|
||||
a {
|
||||
color: rgb(0, 174, 239);
|
||||
|
||||
&:hover {
|
||||
color: rgb(0, 139, 191);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function getState() {
|
||||
const image = selectionState.getImage();
|
||||
return {
|
||||
hasImage: selectionState.hasImage(),
|
||||
imageName: image?.name,
|
||||
imageSize: image?.size,
|
||||
};
|
||||
}
|
||||
|
||||
function isString(value: any): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
const URLSelector = ({
|
||||
done,
|
||||
cancel,
|
||||
}: {
|
||||
done: (imageURL: string, auth?: Authentication) => void;
|
||||
cancel: () => void;
|
||||
}) => {
|
||||
const [imageURL, setImageURL] = React.useState('');
|
||||
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
|
||||
const [username, setUsername] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
const fetchRecentUrlImages = async () => {
|
||||
const recentUrlImages: URL[] = await getRecentUrlImages();
|
||||
setRecentImages(recentUrlImages);
|
||||
};
|
||||
fetchRecentUrlImages();
|
||||
}, []);
|
||||
return (
|
||||
<Modal
|
||||
cancel={cancel}
|
||||
primaryButtonProps={{
|
||||
disabled: loading || !imageURL,
|
||||
}}
|
||||
action={loading ? <Spinner /> : 'OK'}
|
||||
done={async () => {
|
||||
setLoading(true);
|
||||
const urlStrings = recentImages.map((url: URL) => url.href);
|
||||
const normalizedRecentUrls = normalizeRecentUrlImages([
|
||||
...urlStrings,
|
||||
imageURL,
|
||||
]);
|
||||
setRecentUrlImages(normalizedRecentUrls);
|
||||
const auth = username ? { username, password } : undefined;
|
||||
await done(imageURL, auth);
|
||||
}}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
|
||||
<Txt mb="10px" fontSize="24px">
|
||||
Use Image URL
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
fontSize="14px"
|
||||
onClick={() => {
|
||||
if (showBasicAuth) {
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
}
|
||||
setShowBasicAuth(!showBasicAuth);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
{showBasicAuth && (
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
{!showBasicAuth && (
|
||||
<ChevronRightSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
<Txt ml={8}>Authentication</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
{showBasicAuth && (
|
||||
<React.Fragment>
|
||||
<Input
|
||||
mb={15}
|
||||
value={username}
|
||||
placeholder="Enter username"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setUsername(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
placeholder="Enter password"
|
||||
type="password"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPassword(evt.target.value)
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Flex>
|
||||
{recentImages.length > 0 && (
|
||||
<Flex flexDirection="column" height="78.6%">
|
||||
<Txt fontSize={18}>Recent</Txt>
|
||||
<ScrollableFlex flexDirection="column">
|
||||
<Card
|
||||
p="10px 15px"
|
||||
rows={recentImages
|
||||
.map((recent) => (
|
||||
<Txt
|
||||
key={recent.href}
|
||||
onClick={() => {
|
||||
setImageURL(recent.href);
|
||||
}}
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
>
|
||||
{recent.pathname.split('/').pop()} - {recent.href}
|
||||
</Txt>
|
||||
))
|
||||
.reverse()}
|
||||
/>
|
||||
</ScrollableFlex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface Flow {
|
||||
icon?: JSX.Element;
|
||||
onClick: (evt: React.MouseEvent) => void;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const FlowSelector = styled(
|
||||
({ flow, ...props }: { flow: Flow } & ButtonProps) => (
|
||||
<StepButton
|
||||
plain={!props.primary}
|
||||
primary={props.primary}
|
||||
onClick={(evt: React.MouseEvent<Element, MouseEvent>) =>
|
||||
flow.onClick(evt)
|
||||
}
|
||||
icon={flow.icon}
|
||||
{...props}
|
||||
>
|
||||
{flow.label}
|
||||
</StepButton>
|
||||
),
|
||||
)`
|
||||
border-radius: 24px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
:enabled:focus,
|
||||
:enabled:focus svg {
|
||||
color: ${colors.primary.foreground} !important;
|
||||
}
|
||||
|
||||
:enabled:hover {
|
||||
background-color: ${colors.primary.background};
|
||||
color: ${colors.primary.foreground};
|
||||
font-weight: 600;
|
||||
|
||||
svg {
|
||||
color: ${colors.primary.foreground}!important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type Source =
|
||||
| typeof sourceDestination.File
|
||||
| typeof sourceDestination.BlockDevice
|
||||
| typeof sourceDestination.Http;
|
||||
|
||||
export interface SourceMetadata extends sourceDestination.Metadata {
|
||||
hasMBR?: boolean;
|
||||
partitions?: MBRPartition[] | GPTPartition[];
|
||||
path: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
SourceType: Source;
|
||||
drive?: DrivelistDrive;
|
||||
extension?: string;
|
||||
archiveExtension?: string;
|
||||
auth?: Authentication;
|
||||
}
|
||||
|
||||
interface SourceSelectorProps {
|
||||
flashing: boolean;
|
||||
}
|
||||
|
||||
interface SourceSelectorState {
|
||||
hasImage: boolean;
|
||||
imageName?: string;
|
||||
imageSize?: number;
|
||||
warning: { message: string; title: string | null } | null;
|
||||
showImageDetails: boolean;
|
||||
showURLSelector: boolean;
|
||||
showDriveSelector: boolean;
|
||||
defaultFlowActive: boolean;
|
||||
imageSelectorOpen: boolean;
|
||||
imageLoading: boolean;
|
||||
}
|
||||
|
||||
interface Authentication {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class SourceSelector extends React.Component<
|
||||
SourceSelectorProps,
|
||||
SourceSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
|
||||
constructor(props: SourceSelectorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...getState(),
|
||||
warning: null,
|
||||
showImageDetails: false,
|
||||
showURLSelector: false,
|
||||
showDriveSelector: false,
|
||||
defaultFlowActive: true,
|
||||
imageSelectorOpen: false,
|
||||
imageLoading: false,
|
||||
};
|
||||
|
||||
// Bind `this` since it's used in an event's callback
|
||||
this.onSelectImage = this.onSelectImage.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.unsubscribe = observe(() => {
|
||||
this.setState(getState());
|
||||
});
|
||||
ipcRenderer.on('select-image', this.onSelectImage);
|
||||
ipcRenderer.send('source-selector-ready');
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.unsubscribe?.();
|
||||
ipcRenderer.removeListener('select-image', this.onSelectImage);
|
||||
}
|
||||
|
||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||
this.setState({ imageLoading: true });
|
||||
await this.selectSource(
|
||||
imagePath,
|
||||
isURL(this.normalizeImagePath(imagePath))
|
||||
? sourceDestination.Http
|
||||
: sourceDestination.File,
|
||||
).promise;
|
||||
this.setState({ imageLoading: false });
|
||||
}
|
||||
|
||||
private async createSource(
|
||||
selected: string,
|
||||
SourceType: Source,
|
||||
auth?: Authentication,
|
||||
) {
|
||||
try {
|
||||
selected = await replaceWindowsNetworkDriveLetter(selected);
|
||||
} catch (error: any) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
|
||||
if (isJson(decodeURIComponent(selected))) {
|
||||
const config: AxiosRequestConfig = JSON.parse(
|
||||
decodeURIComponent(selected),
|
||||
);
|
||||
return new sourceDestination.Http({
|
||||
url: config.url!,
|
||||
axiosInstance: axios.create(_.omit(config, ['url'])),
|
||||
});
|
||||
}
|
||||
|
||||
if (SourceType === sourceDestination.File) {
|
||||
return new sourceDestination.File({
|
||||
path: selected,
|
||||
});
|
||||
}
|
||||
|
||||
return new sourceDestination.Http({ url: selected, auth });
|
||||
}
|
||||
|
||||
public normalizeImagePath(imgPath: string) {
|
||||
const decodedPath = decodeURIComponent(imgPath);
|
||||
if (isJson(decodedPath)) {
|
||||
return JSON.parse(decodedPath).url ?? decodedPath;
|
||||
}
|
||||
return decodedPath;
|
||||
}
|
||||
|
||||
private reselectSource() {
|
||||
analytics.logEvent('Reselect image', {
|
||||
previousImage: selectionState.getImage(),
|
||||
});
|
||||
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
|
||||
private selectSource(
|
||||
selected: string | DrivelistDrive,
|
||||
SourceType: Source,
|
||||
auth?: Authentication,
|
||||
): { promise: Promise<void>; cancel: () => void } {
|
||||
let cancelled = false;
|
||||
return {
|
||||
cancel: () => {
|
||||
cancelled = true;
|
||||
},
|
||||
promise: (async () => {
|
||||
const sourcePath = isString(selected) ? selected : selected.device;
|
||||
let source;
|
||||
let metadata: SourceMetadata | undefined;
|
||||
if (isString(selected)) {
|
||||
if (
|
||||
SourceType === sourceDestination.Http &&
|
||||
!isURL(this.normalizeImagePath(selected))
|
||||
) {
|
||||
this.handleError(
|
||||
'Unsupported protocol',
|
||||
selected,
|
||||
messages.error.unsupportedProtocol(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
||||
analytics.logEvent('Possibly Windows image', { image: selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.looksLikeWindowsImage(),
|
||||
title: 'Possible Windows image detected',
|
||||
},
|
||||
});
|
||||
}
|
||||
source = await this.createSource(selected, SourceType, auth);
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const innerSource = await source.getInnerSource();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
metadata = await this.getMetadata(innerSource, selected);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
metadata.SourceType = SourceType;
|
||||
|
||||
if (!metadata.hasMBR && this.state.warning === null) {
|
||||
analytics.logEvent('Missing partition table', { metadata });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.missingPartitionTable(),
|
||||
title: 'Missing partition table',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.handleError(
|
||||
'Error opening source',
|
||||
sourcePath,
|
||||
messages.error.openSource(sourcePath, error.message),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
} catch (error: any) {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (selected.partitionTableType === null) {
|
||||
analytics.logEvent('Missing partition table', { selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.driveMissingPartitionTable(),
|
||||
title: 'Missing partition table',
|
||||
},
|
||||
});
|
||||
}
|
||||
metadata = {
|
||||
path: selected.device,
|
||||
displayName: selected.displayName,
|
||||
description: selected.displayName,
|
||||
size: selected.size as SourceMetadata['size'],
|
||||
SourceType: sourceDestination.BlockDevice,
|
||||
drive: selected,
|
||||
};
|
||||
}
|
||||
|
||||
if (metadata !== undefined) {
|
||||
metadata.auth = auth;
|
||||
selectionState.selectSource(metadata);
|
||||
analytics.logEvent('Select image', {
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
// certain features without printing pages of text to DevTools.
|
||||
image: {
|
||||
...metadata,
|
||||
logo: Boolean(metadata.logo),
|
||||
blockMap: Boolean(metadata.blockMap),
|
||||
},
|
||||
});
|
||||
}
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
private handleError(
|
||||
title: string,
|
||||
sourcePath: string,
|
||||
description: string,
|
||||
error?: Error,
|
||||
) {
|
||||
const imageError = errors.createUserError({
|
||||
title,
|
||||
description,
|
||||
});
|
||||
osDialog.showError(imageError);
|
||||
if (error) {
|
||||
analytics.logException(error);
|
||||
return;
|
||||
}
|
||||
analytics.logEvent(title, { path: sourcePath });
|
||||
}
|
||||
|
||||
private async getMetadata(
|
||||
source: sourceDestination.SourceDestination,
|
||||
selected: string | DrivelistDrive,
|
||||
) {
|
||||
const metadata = (await source.getMetadata()) as SourceMetadata;
|
||||
const partitionTable = await source.getPartitionTable();
|
||||
if (partitionTable) {
|
||||
metadata.hasMBR = true;
|
||||
metadata.partitions = partitionTable.partitions;
|
||||
} else {
|
||||
metadata.hasMBR = false;
|
||||
}
|
||||
if (isString(selected)) {
|
||||
metadata.extension = path.extname(selected).slice(1);
|
||||
metadata.path = selected;
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private async openImageSelector() {
|
||||
analytics.logEvent('Open image selector');
|
||||
this.setState({ imageSelectorOpen: true });
|
||||
|
||||
try {
|
||||
const imagePath = await osDialog.selectImage();
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (!imagePath) {
|
||||
analytics.logEvent('Image selector closed');
|
||||
return;
|
||||
}
|
||||
await this.selectSource(imagePath, sourceDestination.File).promise;
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
} finally {
|
||||
this.setState({ imageSelectorOpen: false });
|
||||
}
|
||||
}
|
||||
|
||||
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
|
||||
const [file] = event.dataTransfer.files;
|
||||
if (file) {
|
||||
await this.selectSource(file.path, sourceDestination.File).promise;
|
||||
}
|
||||
}
|
||||
|
||||
private openURLSelector() {
|
||||
analytics.logEvent('Open image URL selector');
|
||||
|
||||
this.setState({
|
||||
showURLSelector: true,
|
||||
});
|
||||
}
|
||||
|
||||
private openDriveSelector() {
|
||||
analytics.logEvent('Open drive selector');
|
||||
|
||||
this.setState({
|
||||
showDriveSelector: true,
|
||||
});
|
||||
}
|
||||
|
||||
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
|
||||
// Needed to get onDrop events on div elements
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private onDragEnter(event: React.DragEvent<HTMLDivElement>) {
|
||||
// Needed to get onDrop events on div elements
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private showSelectedImageDetails() {
|
||||
analytics.logEvent('Show selected image tooltip', {
|
||||
imagePath: selectionState.getImage()?.path,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
showImageDetails: true,
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaultFlowActive(defaultFlowActive: boolean) {
|
||||
this.setState({ defaultFlowActive });
|
||||
}
|
||||
|
||||
private closeModal() {
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO add a visual change when dragging a file over the selector
|
||||
public render() {
|
||||
const { flashing } = this.props;
|
||||
const {
|
||||
showImageDetails,
|
||||
showURLSelector,
|
||||
showDriveSelector,
|
||||
imageLoading,
|
||||
} = this.state;
|
||||
const selectionImage = selectionState.getImage();
|
||||
let image: SourceMetadata | DrivelistDrive =
|
||||
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
||||
|
||||
image = image.drive ?? image;
|
||||
|
||||
let cancelURLSelection = () => {
|
||||
// noop
|
||||
};
|
||||
image.name = image.description || image.name;
|
||||
const imagePath = image.path || image.displayName || '';
|
||||
const imageBasename = path.basename(imagePath);
|
||||
const imageName = image.name || '';
|
||||
const imageSize = image.size;
|
||||
const imageLogo = image.logo || '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
|
||||
onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragEnter(evt)
|
||||
}
|
||||
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragOver(evt)
|
||||
}
|
||||
>
|
||||
<SVGIcon
|
||||
contents={imageLogo}
|
||||
fallback={ImageSvg}
|
||||
style={{
|
||||
marginBottom: 30,
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectionImage !== undefined || imageLoading ? (
|
||||
<>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={() => this.showSelectedImageDetails()}
|
||||
tooltip={imageName || imageBasename}
|
||||
>
|
||||
<Spinner show={imageLoading}>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
</Spinner>
|
||||
</StepNameButton>
|
||||
{!flashing && !imageLoading && (
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={() => this.reselectSource()}
|
||||
>
|
||||
Remove
|
||||
</ChangeButton>
|
||||
)}
|
||||
{!_.isNil(imageSize) && !imageLoading && (
|
||||
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FlowSelector
|
||||
disabled={this.state.imageSelectorOpen}
|
||||
primary={this.state.defaultFlowActive}
|
||||
key="Flash from file"
|
||||
flow={{
|
||||
onClick: () => this.openImageSelector(),
|
||||
label: 'Flash from file',
|
||||
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||
/>
|
||||
<FlowSelector
|
||||
key="Flash from URL"
|
||||
flow={{
|
||||
onClick: () => this.openURLSelector(),
|
||||
label: 'Flash from URL',
|
||||
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||
/>
|
||||
<FlowSelector
|
||||
key="Clone drive"
|
||||
flow={{
|
||||
onClick: () => this.openDriveSelector(),
|
||||
label: 'Clone drive',
|
||||
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{this.state.warning != null && (
|
||||
<SmallModal
|
||||
style={{
|
||||
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
titleElement={
|
||||
<span>
|
||||
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
|
||||
<span>{this.state.warning.title}</span>
|
||||
</span>
|
||||
}
|
||||
action="Continue"
|
||||
cancel={() => {
|
||||
this.setState({ warning: null });
|
||||
this.reselectSource();
|
||||
}}
|
||||
done={() => {
|
||||
this.setState({ warning: null });
|
||||
}}
|
||||
primaryButtonProps={{ warning: true, primary: false }}
|
||||
>
|
||||
<ModalText
|
||||
dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
|
||||
/>
|
||||
</SmallModal>
|
||||
)}
|
||||
|
||||
{showImageDetails && (
|
||||
<SmallModal
|
||||
title="Image"
|
||||
done={() => {
|
||||
this.setState({ showImageDetails: false });
|
||||
}}
|
||||
>
|
||||
<Txt.p>
|
||||
<Txt.span bold>Name: </Txt.span>
|
||||
<Txt.span>{imageName || imageBasename}</Txt.span>
|
||||
</Txt.p>
|
||||
<Txt.p>
|
||||
<Txt.span bold>Path: </Txt.span>
|
||||
<Txt.span>{imagePath}</Txt.span>
|
||||
</Txt.p>
|
||||
</SmallModal>
|
||||
)}
|
||||
|
||||
{showURLSelector && (
|
||||
<URLSelector
|
||||
cancel={() => {
|
||||
cancelURLSelection();
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
done={async (imageURL: string, auth?: Authentication) => {
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (!imageURL) {
|
||||
analytics.logEvent('URL selector closed');
|
||||
} else {
|
||||
let promise;
|
||||
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||
imageURL,
|
||||
sourceDestination.Http,
|
||||
auth,
|
||||
));
|
||||
await promise;
|
||||
}
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDriveSelector && (
|
||||
<DriveSelector
|
||||
write={false}
|
||||
multipleSelection={false}
|
||||
titleLabel="Select source"
|
||||
emptyListLabel="Plug a source drive"
|
||||
emptyListIcon={<SrcSvg width="40px" />}
|
||||
cancel={(originalList) => {
|
||||
if (originalList.length) {
|
||||
const originalSource = originalList[0];
|
||||
if (selectionImage?.drive?.device !== originalSource.device) {
|
||||
this.selectSource(
|
||||
originalSource,
|
||||
sourceDestination.BlockDevice,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
this.closeModal();
|
||||
}}
|
||||
done={() => this.closeModal()}
|
||||
onSelect={(drive) => {
|
||||
if (drive) {
|
||||
if (
|
||||
selectionState.getImage()?.drive?.device === drive?.device
|
||||
) {
|
||||
return selectionState.deselectImage();
|
||||
}
|
||||
this.selectSource(drive, sourceDestination.BlockDevice);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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, 'assets', 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} />;
|
||||
}
|
||||
}
|
||||
|
@@ -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,50 +33,64 @@ 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;
|
||||
openDriveSelector: () => any;
|
||||
reselectDrive: () => any;
|
||||
openDriveSelector: () => void;
|
||||
reselectDrive: () => void;
|
||||
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,23 +108,24 @@ 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>,
|
||||
);
|
||||
}
|
||||
@@ -133,7 +146,8 @@ export function TargetSelector(props: TargetSelectorProps) {
|
||||
|
||||
return (
|
||||
<StepButton
|
||||
tabindex={targets.length > 0 ? -1 : 2}
|
||||
primary
|
||||
tabIndex={targets.length > 0 ? -1 : 2}
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
193
lib/gui/app/components/target-selector/target-selector.tsx
Normal 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>
|
||||
);
|
||||
};
|
BIN
lib/gui/app/css/fonts/SourceSansPro-Regular.ttf
Normal file
BIN
lib/gui/app/css/fonts/SourceSansPro-SemiBold.ttf
Normal 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;
|
||||
}
|
12
lib/gui/app/index.dev.html
Normal 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>
|
@@ -2,13 +2,11 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Etcher</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../node_modules/flexboxgrid/dist/flexboxgrid.css">
|
||||
<link rel="stylesheet" type="text/css" href="../css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="../css/desktop.css">
|
||||
<title>balenaEtcher</title>
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="main"></main>
|
||||
<script src="../../../generated/gui.js"></script>
|
||||
<script src="gui.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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';
|
||||
@@ -26,6 +28,7 @@ import { Actions, store } from './store';
|
||||
export function resetState() {
|
||||
store.dispatch({
|
||||
type: Actions.RESET_FLASH_STATE,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,8 +47,11 @@ 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: {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,6 +70,41 @@ 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[]) {
|
||||
store.dispatch({
|
||||
type: Actions.SET_DEVICE_PATHS,
|
||||
data: devicePaths,
|
||||
});
|
||||
}
|
||||
|
||||
export function addFailedDeviceError({
|
||||
device,
|
||||
error,
|
||||
}: {
|
||||
device: DrivelistDrive;
|
||||
error: Error;
|
||||
}) {
|
||||
const failedDeviceErrorsMap = new Map(
|
||||
store.getState().toJS().failedDeviceErrors,
|
||||
);
|
||||
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_ERRORS,
|
||||
data: Array.from(failedDeviceErrorsMap),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +115,8 @@ export function setProgressState(
|
||||
) {
|
||||
// Preserve only one decimal place
|
||||
const PRECISION = 1;
|
||||
const data = _.assign({}, state, {
|
||||
const data = {
|
||||
...state,
|
||||
percentage:
|
||||
state.percentage !== undefined && _.isFinite(state.percentage)
|
||||
? Math.floor(state.percentage)
|
||||
@@ -87,15 +129,7 @@ export function setProgressState(
|
||||
|
||||
return null;
|
||||
}),
|
||||
|
||||
totalSpeed: _.attempt(() => {
|
||||
if (_.isFinite(state.totalSpeed)) {
|
||||
return _.round(bytesToMegabytes(state.totalSpeed), PRECISION);
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASH_STATE,
|
||||
@@ -108,10 +142,7 @@ export function getFlashResults() {
|
||||
}
|
||||
|
||||
export function getFlashState() {
|
||||
return store
|
||||
.getState()
|
||||
.get('flashState')
|
||||
.toJS();
|
||||
return store.getState().get('flashState').toJS();
|
||||
}
|
||||
|
||||
export function wasLastFlashCancelled() {
|
||||
|
260
lib/gui/app/models/leds.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright 2020 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 { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import {
|
||||
DrivelistDrive,
|
||||
isSourceDrive,
|
||||
} from '../../../shared/drive-constraints';
|
||||
import { getDrives } from './available-drives';
|
||||
import { getSelectedDrives } from './selection-state';
|
||||
import * as settings from './settings';
|
||||
import { observe, store } from './store';
|
||||
|
||||
const leds: Map<string, RGBLed> = new Map();
|
||||
const animator = new Animator([], 10);
|
||||
|
||||
function createAnimationFunction(
|
||||
intensityFunction: (t: number) => number,
|
||||
color: Color,
|
||||
): AnimationFunction {
|
||||
return (t: number): Color => {
|
||||
const intensity = intensityFunction(t);
|
||||
return color.map((v: number) => v * intensity) as Color;
|
||||
};
|
||||
}
|
||||
|
||||
function blink(t: number) {
|
||||
return Math.floor(t) % 2;
|
||||
}
|
||||
|
||||
function one(_t: number) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
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';
|
||||
sourceDrive: string | undefined;
|
||||
availableDrives: string[];
|
||||
selectedDrives: string[];
|
||||
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
|
||||
//
|
||||
// Other slots (2 - 16):
|
||||
//
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
// | | main screen | flashing | validating | results screen |
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
// | no drive | black | black | black | black |
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
// | drive plugged | black | black | black | black |
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
// | drive selected | white | blink purple, red if failed | blink green, red if failed | green if success, red if failed |
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
export function updateLeds({
|
||||
step,
|
||||
sourceDrive,
|
||||
availableDrives,
|
||||
selectedDrives,
|
||||
failedDrives,
|
||||
}: LedsState) {
|
||||
const unplugged = new Set(leds.keys());
|
||||
const plugged = new Set(availableDrives);
|
||||
const selectedOk = new Set(selectedDrives);
|
||||
const selectedFailed = new Set(failedDrives);
|
||||
|
||||
// Remove selected devices from plugged set
|
||||
for (const d of selectedOk) {
|
||||
plugged.delete(d);
|
||||
unplugged.delete(d);
|
||||
}
|
||||
|
||||
// Remove plugged devices from unplugged set
|
||||
for (const d of plugged) {
|
||||
unplugged.delete(d);
|
||||
}
|
||||
|
||||
// Remove failed devices from selected set
|
||||
for (const d of selectedFailed) {
|
||||
selectedOk.delete(d);
|
||||
}
|
||||
|
||||
const mapping: Array<{
|
||||
animation: AnimationFunction;
|
||||
rgbLeds: RGBLed[];
|
||||
}> = [];
|
||||
// Handle source slot
|
||||
if (sourceDrive !== undefined) {
|
||||
if (plugged.has(sourceDrive)) {
|
||||
plugged.delete(sourceDrive);
|
||||
mapping.push(
|
||||
setLeds(ledAnimationFunctions.staticBlue, new Set([sourceDrive])),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (step === 'main') {
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticWhite,
|
||||
new Set([...selectedOk, ...selectedFailed]),
|
||||
),
|
||||
);
|
||||
} else if (step === 'flashing') {
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.blinkPurple, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
} else if (step === 'verifying') {
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.blinkGreen, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
} else if (step === 'finish') {
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.staticGreen, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
}
|
||||
animator.mapping = mapping;
|
||||
}
|
||||
|
||||
let ledsState: LedsState | undefined;
|
||||
|
||||
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 = getDrives().filter(
|
||||
(d: DrivelistDrive) => d.devicePath,
|
||||
);
|
||||
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
|
||||
isSourceDrive(d, s.selection.image),
|
||||
)[0]?.devicePath;
|
||||
const availableDrivesPaths = availableDrives.map(
|
||||
(d: DrivelistDrive) => d.devicePath,
|
||||
);
|
||||
let selectedDrivesPaths: string[];
|
||||
if (step === 'main') {
|
||||
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: failedDevicePaths,
|
||||
} as LedsState;
|
||||
if (!_.isEqual(newLedsState, ledsState)) {
|
||||
updateLeds(newLedsState);
|
||||
ledsState = newLedsState;
|
||||
}
|
||||
}
|
||||
|
||||
export async function init(): Promise<void> {
|
||||
// ledsMapping is something like:
|
||||
// {
|
||||
// 'platform-xhci-hcd.0.auto-usb-0:1.1.1:1.0-scsi-0:0:0:0': [
|
||||
// 'led1_r',
|
||||
// 'led1_g',
|
||||
// 'led1_b',
|
||||
// ],
|
||||
// ...
|
||||
// }
|
||||
const ledsMapping: _.Dictionary<[string, string, string]> =
|
||||
(await settings.get('ledsMapping')) || {};
|
||||
if (!_.isEmpty(ledsMapping)) {
|
||||
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 }));
|
||||
}
|
||||
}
|
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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 electron from 'electron';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const JSON_INDENT = 2;
|
||||
|
||||
/**
|
||||
* @summary Userdata directory path
|
||||
* @description
|
||||
* Defaults to the following:
|
||||
* - `%APPDATA%/etcher` on Windows
|
||||
* - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux
|
||||
* - `~/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
|
||||
*/
|
||||
const USER_DATA_DIR = electron.app
|
||||
? electron.app.getPath('userData')
|
||||
: electron.remote.app.getPath('userData');
|
||||
|
||||
const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
async function readConfigFile(filename: string): Promise<any> {
|
||||
let contents = '{}';
|
||||
try {
|
||||
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(contents);
|
||||
} catch (parseError) {
|
||||
console.error(parseError);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeConfigFile(filename: string, data: any): Promise<any> {
|
||||
await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT));
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function readAll(): Promise<any> {
|
||||
return await readConfigFile(CONFIG_PATH);
|
||||
}
|
||||
|
||||
export async function writeAll(settings: any): Promise<any> {
|
||||
return await writeConfigFile(CONFIG_PATH, settings);
|
||||
}
|
||||
|
||||
export async function clear(): Promise<void> {
|
||||
await fs.unlink(CONFIG_PATH);
|
||||
}
|
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,59 +52,24 @@ export function selectImage(image: any) {
|
||||
* @summary Get all selected drives' devices
|
||||
*/
|
||||
export function getSelectedDevices(): string[] {
|
||||
return store
|
||||
.getState()
|
||||
.getIn(['selection', 'devices'])
|
||||
.toJS();
|
||||
return store.getState().getIn(['selection', 'devices']).toJS();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,19 +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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,5 +124,5 @@ export function isDriveSelected(driveDevice: string) {
|
||||
}
|
||||
|
||||
const selectedDriveDevices = getSelectedDevices();
|
||||
return _.includes(selectedDriveDevices, driveDevice);
|
||||
return selectedDriveDevices.includes(driveDevice);
|
||||
}
|
||||
|
@@ -15,127 +15,113 @@
|
||||
*/
|
||||
|
||||
import * as _debug from 'debug';
|
||||
import * as electron from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as localSettings from './local-settings';
|
||||
|
||||
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
|
||||
* Defaults to the following:
|
||||
* - `%APPDATA%/etcher` on Windows
|
||||
* - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux
|
||||
* - `~/Library/Application Support/etcher` on macOS
|
||||
* See https://electronjs.org/docs/api/app#appgetpathname
|
||||
*
|
||||
* NOTE: We use the remote property when this module
|
||||
* is loaded in the Electron's renderer process
|
||||
*/
|
||||
const app = electron.app || electron.remote.app;
|
||||
|
||||
const USER_DATA_DIR = app.getPath('userData');
|
||||
|
||||
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
||||
let contents = '{}';
|
||||
try {
|
||||
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
||||
} catch (error: any) {
|
||||
// noop
|
||||
}
|
||||
try {
|
||||
return JSON.parse(contents);
|
||||
} catch (parseError) {
|
||||
console.error(parseError);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export async function readAll() {
|
||||
return await readConfigFile(CONFIG_PATH);
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export async function writeConfigFile(
|
||||
filename: string,
|
||||
data: _.Dictionary<any>,
|
||||
): Promise<void> {
|
||||
await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT));
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||
unsafeMode: false,
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true,
|
||||
trim: false,
|
||||
updatesEnabled:
|
||||
packageJSON.updates.enabled &&
|
||||
!_.includes(['rpm', 'deb'], packageJSON.packageType),
|
||||
lastSleptUpdateNotifier: null,
|
||||
lastSleptUpdateNotifierVersion: null,
|
||||
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
|
||||
desktopNotifications: true,
|
||||
autoBlockmapping: true,
|
||||
decompressFirst: true,
|
||||
};
|
||||
|
||||
let settings = _.cloneDeep(DEFAULT_SETTINGS);
|
||||
const settings = _.cloneDeep(DEFAULT_SETTINGS);
|
||||
|
||||
/**
|
||||
* @summary Reset settings to their default values
|
||||
*/
|
||||
export async function reset(): Promise<void> {
|
||||
debug('reset');
|
||||
// TODO: Remove default settings from config file (?)
|
||||
settings = _.cloneDeep(DEFAULT_SETTINGS);
|
||||
return await localSettings.writeAll(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the current settings
|
||||
*/
|
||||
export async function assign(value: _.Dictionary<any>): Promise<void> {
|
||||
debug('assign', value);
|
||||
if (_.isNil(value)) {
|
||||
throw errors.createError({
|
||||
title: 'Missing settings',
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(value)) {
|
||||
throw errors.createError({
|
||||
title: 'Settings must be an object',
|
||||
});
|
||||
}
|
||||
|
||||
const newSettings = _.assign({}, settings, value);
|
||||
|
||||
const updatedSettings = await localSettings.writeAll(newSettings);
|
||||
// NOTE: Only update in memory settings when successfully written
|
||||
settings = updatedSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the application state with the local settings
|
||||
*/
|
||||
export async function load(): Promise<void> {
|
||||
async function load(): Promise<void> {
|
||||
debug('load');
|
||||
const loadedSettings = await localSettings.readAll();
|
||||
const loadedSettings = await readAll();
|
||||
_.assign(settings, loadedSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Set a setting value
|
||||
*/
|
||||
export async function set(key: string, value: any): Promise<void> {
|
||||
const loaded = load();
|
||||
|
||||
export async function set(
|
||||
key: string,
|
||||
value: any,
|
||||
writeConfigFileFn = writeConfigFile,
|
||||
): Promise<void> {
|
||||
debug('set', key, value);
|
||||
if (_.isNil(key)) {
|
||||
throw errors.createError({
|
||||
title: 'Missing setting key',
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isString(key)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid setting key: ${key}`,
|
||||
});
|
||||
}
|
||||
|
||||
await loaded;
|
||||
const previousValue = settings[key];
|
||||
settings[key] = value;
|
||||
try {
|
||||
await localSettings.writeAll(settings);
|
||||
} catch (error) {
|
||||
await writeConfigFileFn(CONFIG_PATH, settings);
|
||||
} catch (error: any) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a setting value
|
||||
*/
|
||||
export function get(key: string): any {
|
||||
return _.cloneDeep(_.get(settings, [key]));
|
||||
export async function get(key: string): Promise<any> {
|
||||
await loaded;
|
||||
return getSync(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if setting value exists
|
||||
*/
|
||||
export function has(key: string): boolean {
|
||||
return settings[key] != null;
|
||||
export function getSync(key: string): any {
|
||||
return _.cloneDeep(settings[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get all setting values
|
||||
*/
|
||||
export function getAll() {
|
||||
export async function getAll() {
|
||||
debug('getAll');
|
||||
await loaded;
|
||||
return _.cloneDeep(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the default setting values
|
||||
*/
|
||||
export function getDefaults() {
|
||||
debug('getDefaults');
|
||||
return _.cloneDeep(DEFAULT_SETTINGS);
|
||||
}
|
||||
|
@@ -16,13 +16,12 @@
|
||||
|
||||
import * as Immutable from 'immutable';
|
||||
import * as _ from 'lodash';
|
||||
import { basename } from 'path';
|
||||
import * as redux from 'redux';
|
||||
import * as uuidV4 from 'uuid/v4';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as constraints from '../../../shared/drive-constraints';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as fileExtensions from '../../../shared/file-extensions';
|
||||
import * as supportedFormats from '../../../shared/supported-formats';
|
||||
import * as utils from '../../../shared/utils';
|
||||
import * as settings from './settings';
|
||||
|
||||
@@ -34,7 +33,7 @@ function verifyNoNilFields(
|
||||
fields: string[],
|
||||
name: string,
|
||||
) {
|
||||
const nilFields = _.filter(fields, field => {
|
||||
const nilFields = _.filter(fields, (field) => {
|
||||
return _.isNil(_.get(object, field));
|
||||
});
|
||||
if (nilFields.length) {
|
||||
@@ -45,7 +44,7 @@ function verifyNoNilFields(
|
||||
/**
|
||||
* @summary FLASH_STATE fields that can't be nil
|
||||
*/
|
||||
const flashStateNoNilFields = ['speed', 'totalSpeed'];
|
||||
const flashStateNoNilFields = ['speed'];
|
||||
|
||||
/**
|
||||
* @summary SELECT_IMAGE fields that can't be nil
|
||||
@@ -55,7 +54,7 @@ const selectImageNoNilFields = ['path', 'extension'];
|
||||
/**
|
||||
* @summary Application default state
|
||||
*/
|
||||
const DEFAULT_STATE = Immutable.fromJS({
|
||||
export const DEFAULT_STATE = Immutable.fromJS({
|
||||
applicationSessionUuid: '',
|
||||
flashingWorkflowUuid: '',
|
||||
availableDrives: [],
|
||||
@@ -63,31 +62,34 @@ const DEFAULT_STATE = Immutable.fromJS({
|
||||
devices: Immutable.OrderedSet(),
|
||||
},
|
||||
isFlashing: false,
|
||||
devicePaths: [],
|
||||
failedDeviceErrors: [],
|
||||
flashResults: {},
|
||||
flashState: {
|
||||
flashing: 0,
|
||||
verifying: 0,
|
||||
successful: 0,
|
||||
active: 0,
|
||||
failed: 0,
|
||||
percentage: 0,
|
||||
speed: null,
|
||||
totalSpeed: null,
|
||||
averageSpeed: null,
|
||||
},
|
||||
lastAverageFlashingSpeed: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Application supported action messages
|
||||
*/
|
||||
export enum Actions {
|
||||
SET_AVAILABLE_DRIVES,
|
||||
SET_DEVICE_PATHS,
|
||||
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,
|
||||
}
|
||||
@@ -115,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) {
|
||||
@@ -124,7 +126,7 @@ function storeReducer(
|
||||
});
|
||||
}
|
||||
|
||||
const drives = action.data;
|
||||
let drives = action.data;
|
||||
|
||||
if (!_.isArray(drives) || !_.every(drives, _.isObject)) {
|
||||
throw errors.createError({
|
||||
@@ -132,6 +134,20 @@ 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,
|
||||
]);
|
||||
|
||||
const newState = state.set('availableDrives', Immutable.fromJS(drives));
|
||||
const selectedDevices = newState.getIn(['selection', 'devices']).toJS();
|
||||
|
||||
@@ -148,7 +164,7 @@ function storeReducer(
|
||||
) {
|
||||
// Deselect this drive gone from availableDrives
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@@ -159,7 +175,7 @@ function storeReducer(
|
||||
);
|
||||
|
||||
const shouldAutoselectAll = Boolean(
|
||||
settings.get('disableExplicitDriveSelection'),
|
||||
settings.getSync('autoSelectAllDrives'),
|
||||
);
|
||||
const AUTOSELECT_DRIVE_COUNT = 1;
|
||||
const nonStaleSelectedDevices = nonStaleNewState
|
||||
@@ -181,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,
|
||||
});
|
||||
},
|
||||
@@ -225,17 +236,7 @@ function storeReducer(
|
||||
|
||||
verifyNoNilFields(action.data, flashStateNoNilFields, 'flash');
|
||||
|
||||
if (
|
||||
!_.every(
|
||||
_.pick(action.data, [
|
||||
'flashing',
|
||||
'verifying',
|
||||
'successful',
|
||||
'failed',
|
||||
]),
|
||||
_.isFinite,
|
||||
)
|
||||
) {
|
||||
if (!_.every(_.pick(action.data, ['active', 'failed']), _.isFinite)) {
|
||||
throw errors.createError({
|
||||
title: 'State quantity field(s) not finite number',
|
||||
});
|
||||
@@ -256,7 +257,11 @@ function storeReducer(
|
||||
});
|
||||
}
|
||||
|
||||
return state.set('flashState', Immutable.fromJS(action.data));
|
||||
let ret = state.set('flashState', Immutable.fromJS(action.data));
|
||||
if (action.data.type === 'flashing') {
|
||||
ret = ret.set('lastAverageFlashingSpeed', action.data.averageSpeed);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
case Actions.RESET_FLASH_STATE: {
|
||||
@@ -264,6 +269,12 @@ function storeReducer(
|
||||
.set('isFlashing', false)
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'))
|
||||
.set('flashResults', DEFAULT_STATE.get('flashResults'))
|
||||
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
|
||||
.set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
|
||||
.set(
|
||||
'lastAverageFlashingSpeed',
|
||||
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
|
||||
)
|
||||
.delete('flashUuid');
|
||||
}
|
||||
|
||||
@@ -285,6 +296,7 @@ function storeReducer(
|
||||
|
||||
_.defaults(action.data, {
|
||||
cancelled: false,
|
||||
skip: false,
|
||||
});
|
||||
|
||||
if (!_.isBoolean(action.data.cancelled)) {
|
||||
@@ -319,13 +331,25 @@ function storeReducer(
|
||||
});
|
||||
}
|
||||
|
||||
if (action.data.results) {
|
||||
action.data.results.averageFlashingSpeed = state.get(
|
||||
'lastAverageFlashingSpeed',
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -375,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({
|
||||
@@ -386,51 +412,6 @@ function storeReducer(
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isString(action.data.extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`,
|
||||
});
|
||||
}
|
||||
|
||||
const extension = _.toLower(action.data.extension);
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`,
|
||||
});
|
||||
}
|
||||
|
||||
let lastImageExtension = fileExtensions.getLastFileExtension(
|
||||
action.data.path,
|
||||
);
|
||||
lastImageExtension = _.isString(lastImageExtension)
|
||||
? _.toLower(lastImageExtension)
|
||||
: lastImageExtension;
|
||||
|
||||
if (lastImageExtension !== extension) {
|
||||
if (!_.isString(action.data.archiveExtension)) {
|
||||
throw errors.createError({
|
||||
title: 'Missing image archive extension',
|
||||
});
|
||||
}
|
||||
|
||||
const archiveExtension = _.toLower(action.data.archiveExtension);
|
||||
|
||||
if (
|
||||
!_.includes(supportedFormats.getAllExtensions(), archiveExtension)
|
||||
) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image archive extension: ${action.data.archiveExtension}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (lastImageExtension !== archiveExtension) {
|
||||
throw errors.createError({
|
||||
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const MINIMUM_IMAGE_SIZE = 0;
|
||||
|
||||
if (action.data.size !== undefined) {
|
||||
@@ -485,7 +466,7 @@ function storeReducer(
|
||||
!constraints.isDriveSizeRecommended(drive, action.data)
|
||||
) {
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@@ -496,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) {
|
||||
@@ -520,7 +501,7 @@ function storeReducer(
|
||||
);
|
||||
}
|
||||
|
||||
case Actions.DESELECT_IMAGE: {
|
||||
case Actions.DESELECT_SOURCE: {
|
||||
return state.deleteIn(['selection', 'image']);
|
||||
}
|
||||
|
||||
@@ -532,6 +513,14 @@ function storeReducer(
|
||||
return state.set('flashingWorkflowUuid', action.data);
|
||||
}
|
||||
|
||||
case Actions.SET_DEVICE_PATHS: {
|
||||
return state.set('devicePaths', action.data);
|
||||
}
|
||||
|
||||
case Actions.SET_FAILED_DEVICE_ERRORS: {
|
||||
return state.set('failedDeviceErrors', action.data);
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
|
@@ -18,36 +18,33 @@ 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';
|
||||
|
||||
const sentryToken =
|
||||
settings.get('analyticsSentryToken') ||
|
||||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
|
||||
const mixpanelToken =
|
||||
settings.get('analyticsMixpanelToken') ||
|
||||
_.get(packageJSON, ['analytics', 'mixpanel', 'token']);
|
||||
|
||||
const configUrl =
|
||||
settings.get('configUrl') || 'https://balena.io/etcher/static/config.json';
|
||||
import { store } from '../models/store';
|
||||
|
||||
const DEFAULT_PROBABILITY = 0.1;
|
||||
|
||||
const services = {
|
||||
sentry: sentryToken,
|
||||
mixpanel: mixpanelToken,
|
||||
};
|
||||
|
||||
resinCorvus.install({
|
||||
services,
|
||||
options: {
|
||||
release: packageJSON.version,
|
||||
shouldReport: () => {
|
||||
return settings.get('errorReporting');
|
||||
async function installCorvus(): Promise<void> {
|
||||
const sentryToken =
|
||||
(await settings.get('analyticsSentryToken')) ||
|
||||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
|
||||
const mixpanelToken =
|
||||
(await settings.get('analyticsMixpanelToken')) ||
|
||||
_.get(packageJSON, ['analytics', 'mixpanel', 'token']);
|
||||
resinCorvus.install({
|
||||
services: {
|
||||
sentry: sentryToken,
|
||||
mixpanel: mixpanelToken,
|
||||
},
|
||||
mixpanelDeferred: true,
|
||||
},
|
||||
});
|
||||
options: {
|
||||
release: packageJSON.version,
|
||||
shouldReport: () => {
|
||||
return settings.getSync('errorReporting');
|
||||
},
|
||||
mixpanelDeferred: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let mixpanelSample = DEFAULT_PROBABILITY;
|
||||
|
||||
@@ -55,8 +52,10 @@ let mixpanelSample = DEFAULT_PROBABILITY;
|
||||
* @summary Init analytics configurations
|
||||
*/
|
||||
async function initConfig() {
|
||||
await installCorvus();
|
||||
let validatedConfig = null;
|
||||
try {
|
||||
const configUrl = await settings.get('configUrl');
|
||||
const config = await getConfig(configUrl);
|
||||
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
|
||||
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
|
||||
@@ -90,28 +89,28 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Log a debug message
|
||||
*
|
||||
* @description
|
||||
* This function sends the debug message to error reporting services.
|
||||
*/
|
||||
export const logDebug = resinCorvus.logDebug;
|
||||
|
||||
/**
|
||||
* @summary Log an event
|
||||
*
|
||||
* @description
|
||||
* This function sends the debug message to product analytics services.
|
||||
*/
|
||||
export function logEvent(message: string, data: any) {
|
||||
resinCorvus.logEvent(message, { ...data, sample: mixpanelSample });
|
||||
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
|
||||
const { applicationSessionUuid, flashingWorkflowUuid } = store
|
||||
.getState()
|
||||
.toJS();
|
||||
resinCorvus.logEvent(message, {
|
||||
...data,
|
||||
sample: mixpanelSample,
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -15,32 +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.get('unsafeMode') && !settings.get('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);
|
||||
|
@@ -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,13 +24,13 @@ import * as path from 'path';
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as permissions from '../../../shared/permissions';
|
||||
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';
|
||||
import { store } from '../models/store';
|
||||
import * as analytics from '../modules/analytics';
|
||||
import * as windowProgress from '../os/window-progress';
|
||||
import { updateLock } from './update-lock';
|
||||
|
||||
const THREADS_PER_CPU = 16;
|
||||
|
||||
@@ -60,8 +59,6 @@ function handleErrorLogging(
|
||||
) {
|
||||
const eventData = {
|
||||
...analyticsData,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
};
|
||||
|
||||
@@ -96,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
|
||||
@@ -130,30 +127,35 @@ 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 function performWrite(
|
||||
image: string,
|
||||
async function performWrite(
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
onProgress: sdk.multiWrite.OnProgressFunction,
|
||||
): Promise<{ cancelled?: boolean }> {
|
||||
let cancelled = false;
|
||||
let skip = false;
|
||||
ipc.serve();
|
||||
return new Promise((resolve, reject) => {
|
||||
ipc.server.on('error', error => {
|
||||
const { autoBlockmapping, decompressFirst } = await settings.getAll();
|
||||
return await new Promise((resolve, reject) => {
|
||||
ipc.server.on('error', (error) => {
|
||||
terminateServer();
|
||||
const errorObject = errors.fromJSON(error);
|
||||
reject(errorObject);
|
||||
});
|
||||
|
||||
ipc.server.on('log', message => {
|
||||
ipc.server.on('log', (message) => {
|
||||
console.log(message);
|
||||
});
|
||||
|
||||
@@ -164,20 +166,22 @@ export function performWrite(
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
trim: settings.get('trim'),
|
||||
};
|
||||
|
||||
ipc.server.on('fail', ({ error }) => {
|
||||
ipc.server.on('fail', ({ device, error }) => {
|
||||
if (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);
|
||||
ipc.server.on('done', (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', () => {
|
||||
@@ -185,23 +189,27 @@ export function performWrite(
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
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,
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
trim: settings.get('trim'),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
SourceType: image.SourceType.name,
|
||||
autoBlockmapping,
|
||||
decompressFirst,
|
||||
});
|
||||
});
|
||||
|
||||
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, {
|
||||
@@ -209,7 +217,8 @@ export 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) {
|
||||
@@ -222,24 +231,26 @@ export 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
|
||||
) {
|
||||
throw errors.createUserError({
|
||||
title: 'The writer process ended unexpectedly',
|
||||
description:
|
||||
'Please try again, and contact the Etcher team if the problem persists',
|
||||
code: 'ECHILDDIED',
|
||||
});
|
||||
reject(
|
||||
errors.createUserError({
|
||||
title: 'The writer process ended unexpectedly',
|
||||
description:
|
||||
'Please try again, and contact the Etcher team if the problem persists',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve(flashResults);
|
||||
});
|
||||
|
||||
// Clear the update lock timer to prevent longer
|
||||
// flashing timing it out, and releasing the lock
|
||||
updateLock.pause();
|
||||
ipc.server.start();
|
||||
});
|
||||
}
|
||||
@@ -248,14 +259,19 @@ export function performWrite(
|
||||
* @summary Flash an image to drives
|
||||
*/
|
||||
export async function flash(
|
||||
image: string,
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
// 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[],
|
||||
);
|
||||
|
||||
const analyticsData = {
|
||||
image,
|
||||
@@ -264,28 +280,20 @@ export async function flash(
|
||||
uuid: flashState.getFlashUuid(),
|
||||
status: 'started',
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
trim: settings.get('trim'),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
};
|
||||
|
||||
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,
|
||||
);
|
||||
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,
|
||||
@@ -304,12 +312,14 @@ export async function flash(
|
||||
};
|
||||
analytics.logEvent('Elevation cancelled', eventData);
|
||||
} else {
|
||||
const { results } = flashState.getFlashResults();
|
||||
const { results = {} } = flashState.getFlashResults();
|
||||
const eventData = {
|
||||
...analyticsData,
|
||||
errors: results.errors,
|
||||
devices: results.devices,
|
||||
status: 'finished',
|
||||
bytesWritten: results.bytesWritten,
|
||||
sourceMetadata: results.sourceMetadata,
|
||||
};
|
||||
analytics.logEvent('Done', eventData);
|
||||
}
|
||||
@@ -318,33 +328,28 @@ export async function flash(
|
||||
/**
|
||||
* @summary Cancel write operation
|
||||
*/
|
||||
export 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: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
trim: settings.get('trim'),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
status: 'cancel',
|
||||
status,
|
||||
};
|
||||
analytics.logEvent('Cancel', analyticsData);
|
||||
|
||||
// Re-enable lock release on inactivity
|
||||
updateLock.resume();
|
||||
|
||||
try {
|
||||
// @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);
|
||||
}
|
||||
}
|
||||
|
@@ -14,67 +14,64 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { bytesToClosestUnit } from '../../../shared/units';
|
||||
import * as settings from '../models/settings';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
export interface FlashState {
|
||||
flashing: number;
|
||||
verifying: number;
|
||||
successful: number;
|
||||
active: number;
|
||||
failed: number;
|
||||
percentage?: number;
|
||||
speed: number;
|
||||
position: number;
|
||||
type?: 'decompressing' | 'flashing' | 'verifying';
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Make the progress status subtitle string
|
||||
*
|
||||
* @param {Object} state - flashing metadata
|
||||
*
|
||||
* @returns {String}
|
||||
*
|
||||
* @example
|
||||
* const status = progressStatus.fromFlashState({
|
||||
* flashing: 1,
|
||||
* verifying: 0,
|
||||
* successful: 0,
|
||||
* failed: 0,
|
||||
* percentage: 55,
|
||||
* speed: 2049
|
||||
* })
|
||||
*
|
||||
* console.log(status)
|
||||
* // '55% Flashing'
|
||||
*/
|
||||
export function fromFlashState(state: FlashState): string {
|
||||
const isFlashing = Boolean(state.flashing);
|
||||
const isValidating = !isFlashing && Boolean(state.verifying);
|
||||
const shouldValidate = settings.get('validateWriteOnSuccess');
|
||||
const shouldUnmount = settings.get('unmountOnSuccess');
|
||||
|
||||
if (state.percentage === 0 && !state.speed) {
|
||||
if (isValidating) {
|
||||
return 'Validating...';
|
||||
export function fromFlashState({
|
||||
type,
|
||||
percentage,
|
||||
position,
|
||||
}: Pick<FlashState, 'type' | 'percentage' | 'position'>): {
|
||||
status: string;
|
||||
position?: string;
|
||||
} {
|
||||
if (type === undefined) {
|
||||
return { status: 'Starting...' };
|
||||
} else if (type === 'decompressing') {
|
||||
if (percentage == null) {
|
||||
return { status: 'Decompressing...' };
|
||||
} else {
|
||||
return { position: `${percentage}%`, status: 'Decompressing...' };
|
||||
}
|
||||
|
||||
return 'Starting...';
|
||||
} else if (state.percentage === 100) {
|
||||
if ((isValidating || !shouldValidate) && shouldUnmount) {
|
||||
return 'Unmounting...';
|
||||
} else if (type === 'flashing') {
|
||||
if (percentage != null) {
|
||||
if (percentage < 100) {
|
||||
return { position: `${percentage}%`, status: 'Flashing...' };
|
||||
} else {
|
||||
return { status: 'Finishing...' };
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: 'Flashing...',
|
||||
position: `${position ? prettyBytes(position) : ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
return 'Finishing...';
|
||||
} else if (isFlashing) {
|
||||
if (state.percentage != null) {
|
||||
return `${state.percentage}% Flashing`;
|
||||
} else if (type === 'verifying') {
|
||||
if (percentage == null) {
|
||||
return { status: 'Validating...' };
|
||||
} else if (percentage < 100) {
|
||||
return { position: `${percentage}%`, status: 'Validating...' };
|
||||
} else {
|
||||
return { status: 'Finishing...' };
|
||||
}
|
||||
return `${bytesToClosestUnit(state.position)} flashed`;
|
||||
} else if (isValidating) {
|
||||
return `${state.percentage}% Validating`;
|
||||
} else if (!isFlashing && !isValidating) {
|
||||
return 'Failed';
|
||||
}
|
||||
|
||||
throw new Error(`Invalid state: ${JSON.stringify(state)}`);
|
||||
return { status: 'Failed' };
|
||||
}
|
||||
|
||||
export function titleFromFlashState(
|
||||
state: Pick<FlashState, 'type' | 'percentage' | 'position'>,
|
||||
): string {
|
||||
const { status, position } = fromFlashState(state);
|
||||
if (position !== undefined) {
|
||||
return `${position} ${status}`;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 _debug from 'debug';
|
||||
import * as electron from 'electron';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as createInactivityTimer from 'inactivity-timer';
|
||||
|
||||
import * as settings from '../models/settings';
|
||||
import { logException } from './analytics';
|
||||
|
||||
const debug = _debug('etcher:update-lock');
|
||||
|
||||
/**
|
||||
* Interaction timeout in milliseconds (defaults to 5 minutes)
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const INTERACTION_TIMEOUT_MS = settings.has('interactionTimeout')
|
||||
? parseInt(settings.get('interactionTimeout'), 10)
|
||||
: 5 * 60 * 1000;
|
||||
|
||||
class UpdateLock extends EventEmitter {
|
||||
private paused: boolean;
|
||||
private lockTimer: any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.paused = false;
|
||||
this.on('inactive', UpdateLock.onInactive);
|
||||
this.lockTimer = createInactivityTimer(INTERACTION_TIMEOUT_MS, () => {
|
||||
debug('inactive');
|
||||
this.emit('inactive');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Inactivity event handler, releases the balena update lock on inactivity
|
||||
*/
|
||||
private static onInactive() {
|
||||
if (settings.get('resinUpdateLock')) {
|
||||
UpdateLock.check((checkError: Error, isLocked: boolean) => {
|
||||
debug('inactive-check', Boolean(checkError));
|
||||
if (checkError) {
|
||||
logException(checkError);
|
||||
}
|
||||
if (isLocked) {
|
||||
UpdateLock.release((error?: Error) => {
|
||||
debug('inactive-release', Boolean(error));
|
||||
if (error) {
|
||||
logException(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Acquire the update lock
|
||||
*/
|
||||
private static acquire(callback: (error?: Error) => void) {
|
||||
debug('lock');
|
||||
if (settings.get('resinUpdateLock')) {
|
||||
electron.ipcRenderer.once('resin-update-lock', (_event, error) => {
|
||||
callback(error);
|
||||
});
|
||||
electron.ipcRenderer.send('resin-update-lock', 'lock');
|
||||
} else {
|
||||
callback(new Error('Update lock disabled'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Release the update lock
|
||||
*/
|
||||
public static release(callback: (error?: Error) => void) {
|
||||
debug('unlock');
|
||||
if (settings.get('resinUpdateLock')) {
|
||||
electron.ipcRenderer.once('resin-update-lock', (_event, error) => {
|
||||
callback(error);
|
||||
});
|
||||
electron.ipcRenderer.send('resin-update-lock', 'unlock');
|
||||
} else {
|
||||
callback(new Error('Update lock disabled'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check the state of the update lock
|
||||
* @param {Function} callback - callback(error, isLocked)
|
||||
* @example
|
||||
* UpdateLock.check((error, isLocked) => {
|
||||
* if (isLocked) {
|
||||
* // ...
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
private static check(
|
||||
callback: (error: Error | null, isLocked?: boolean) => void,
|
||||
) {
|
||||
debug('check');
|
||||
if (settings.get('resinUpdateLock')) {
|
||||
electron.ipcRenderer.once(
|
||||
'resin-update-lock',
|
||||
(_event, error, isLocked) => {
|
||||
callback(error, isLocked);
|
||||
},
|
||||
);
|
||||
electron.ipcRenderer.send('resin-update-lock', 'check');
|
||||
} else {
|
||||
callback(new Error('Update lock disabled'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the lock timer
|
||||
*/
|
||||
public extend() {
|
||||
debug('extend');
|
||||
|
||||
if (this.paused) {
|
||||
debug('extend:paused');
|
||||
return;
|
||||
}
|
||||
|
||||
this.lockTimer.signal();
|
||||
|
||||
// When extending, check that we have the lock,
|
||||
// and acquire it, if not
|
||||
if (settings.get('resinUpdateLock')) {
|
||||
UpdateLock.check((checkError, isLocked) => {
|
||||
if (checkError) {
|
||||
logException(checkError);
|
||||
}
|
||||
if (!isLocked) {
|
||||
UpdateLock.acquire(error => {
|
||||
if (error) {
|
||||
logException(error);
|
||||
}
|
||||
debug('extend-acquire', Boolean(error));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Clear the lock timer
|
||||
*/
|
||||
private clearTimer() {
|
||||
debug('clear');
|
||||
this.lockTimer.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Clear the lock timer, and pause extension, avoiding triggering until resume()d
|
||||
*/
|
||||
public pause() {
|
||||
debug('pause');
|
||||
this.paused = true;
|
||||
this.clearTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Un-pause lock extension, and restart the timer
|
||||
*/
|
||||
public resume() {
|
||||
debug('resume');
|
||||
this.paused = false;
|
||||
this.extend();
|
||||
}
|
||||
}
|
||||
|
||||
export const updateLock = new UpdateLock();
|
@@ -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,15 +54,18 @@ export async function selectImage(): Promise<string | undefined> {
|
||||
filters: [
|
||||
{
|
||||
name: 'OS Images',
|
||||
extensions: [...getAllExtensions()].sort(),
|
||||
extensions: SUPPORTED_EXTENSIONS,
|
||||
},
|
||||
{
|
||||
name: 'All',
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
};
|
||||
const currentWindow = electron.remote.getCurrentWindow();
|
||||
const [file] = (await electron.remote.dialog.showOpenDialog(
|
||||
currentWindow,
|
||||
options,
|
||||
)).filePaths;
|
||||
const [file] = (
|
||||
await electron.remote.dialog.showOpenDialog(currentWindow, options)
|
||||
).filePaths;
|
||||
return file;
|
||||
}
|
||||
|
||||
|
@@ -21,9 +21,9 @@ import * as settings from '../models/settings';
|
||||
/**
|
||||
* @summary Send a notification
|
||||
*/
|
||||
export function send(title: string, body: string, icon: string) {
|
||||
export async function send(title: string, body: string, icon: string) {
|
||||
// Bail out if desktop notifications are disabled
|
||||
if (!settings.get('desktopNotifications')) {
|
||||
if (!(await settings.get('desktopNotifications'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -16,22 +16,18 @@
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as settings from '../../../models/settings';
|
||||
import { store } from '../../../models/store';
|
||||
import { logEvent } from '../../../modules/analytics';
|
||||
|
||||
/**
|
||||
* @summary Open an external resource
|
||||
*/
|
||||
export function open(url: string) {
|
||||
export async function open(url: string) {
|
||||
// Don't open links if they're disabled by the env var
|
||||
if (settings.get('disableExternalLinks')) {
|
||||
if (await settings.get('disableExternalLinks')) {
|
||||
return;
|
||||
}
|
||||
|
||||
logEvent('Open external link', {
|
||||
url,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
});
|
||||
logEvent('Open external link', { url });
|
||||
|
||||
if (url) {
|
||||
electron.shell.openExternal(url);
|
||||
|
@@ -17,7 +17,7 @@
|
||||
import * as electron from 'electron';
|
||||
|
||||
import { percentageToFloat } from '../../../shared/utils';
|
||||
import { FlashState, fromFlashState } from '../modules/progress-status';
|
||||
import { FlashState, titleFromFlashState } from '../modules/progress-status';
|
||||
|
||||
/**
|
||||
* @summary The title of the main window upon program launch
|
||||
@@ -29,7 +29,7 @@ const INITIAL_TITLE = document.title;
|
||||
*/
|
||||
function getWindowTitle(state?: FlashState) {
|
||||
if (state) {
|
||||
return `${INITIAL_TITLE} – ${fromFlashState(state)}`;
|
||||
return `${INITIAL_TITLE} – ${titleFromFlashState(state)}`;
|
||||
}
|
||||
return INITIAL_TITLE;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -88,7 +86,7 @@ async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
|
||||
trim(str.slice(colonPosition + 1)),
|
||||
];
|
||||
})
|
||||
.filter(couple => couple[1].length > 0)
|
||||
.filter((couple) => couple[1].length > 0)
|
||||
.value();
|
||||
return new Map(couples);
|
||||
}
|
||||
@@ -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}`;
|
||||
|
@@ -1,174 +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;
|
||||
}
|
||||
|
||||
.col-xs-5.inline-flex.items-baseline > span, .col-xs-5.inline-flex.items-baseline > div {
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
.page-finish .button-label {
|
||||
margin: 0 auto $spacing-medium;
|
||||
|
||||
// Keep some spacing at the sides
|
||||
max-width: $btn-min-width - 5px;
|
||||
}
|
||||
|
||||
.page-finish .button-primary {
|
||||
min-width: $btn-min-width;
|
||||
}
|
||||
|
||||
.page-finish .title,
|
||||
.page-finish .title h3 {
|
||||
color: $palette-theme-dark-foreground;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.page-finish .huge-title {
|
||||
font-size: 3.5em;
|
||||
}
|
||||
|
||||
.page-finish .label {
|
||||
display: inline-block;
|
||||
|
||||
> b {
|
||||
color: $palette-theme-dark-soft-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.page-finish .soft {
|
||||
color: $palette-theme-dark-soft-foreground;
|
||||
}
|
||||
|
||||
.page-finish .separator-xs {
|
||||
flex-grow: 0;
|
||||
background-color: $palette-theme-dark-soft-background;
|
||||
padding: 0px;
|
||||
min-width: 2px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.items-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.page-finish .tick--success {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
margin-left: 5px;
|
||||
|
||||
> .title {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
@@ -1,141 +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, store } 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.customColors.dark.disabled.foreground
|
||||
: props.theme.customColors.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.get('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={['../../assets/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', {
|
||||
applicationSessionUuid: store.getState().toJS()
|
||||
.applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS()
|
||||
.flashingWorkflowUuid,
|
||||
});
|
||||
setShowDriveSelectorModal(true);
|
||||
}}
|
||||
flashing={flashing}
|
||||
targets={targets}
|
||||
image={image}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showDriveSelectorModal && (
|
||||
<DriveSelectorModal
|
||||
close={() => setShowDriveSelectorModal(false)}
|
||||
></DriveSelectorModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -14,45 +14,33 @@
|
||||
* 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 { 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 { 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';
|
||||
import { store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { scanner as driveScanner } from '../../modules/drive-scanner';
|
||||
import * as imageWriter from '../../modules/image-writer';
|
||||
import * as progressStatus from '../../modules/progress-status';
|
||||
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,14 +59,38 @@ const getErrorMessageFromCode = (errorCode: string) => {
|
||||
return '';
|
||||
};
|
||||
|
||||
const flashImageToDrive = async (goToSuccess: () => void) => {
|
||||
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,
|
||||
): 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 || flashState.isFlashing()) {
|
||||
if (drives.length === 0 || isFlashing) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -86,42 +98,33 @@ const flashImageToDrive = async (goToSuccess: () => void) => {
|
||||
// otherwise Windows throws EPERM
|
||||
driveScanner.stop();
|
||||
|
||||
const iconPath = '../../assets/icon.png';
|
||||
const iconPath = path.join('media', 'icon.png');
|
||||
const basename = path.basename(image.path);
|
||||
try {
|
||||
await imageWriter.flash(image.path, drives);
|
||||
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) {
|
||||
// When flashing is cancelled before starting above there is no error
|
||||
if (!error) {
|
||||
return '';
|
||||
}
|
||||
|
||||
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;
|
||||
analytics.logException(error);
|
||||
errorMessage = messages.error.genericFlashError();
|
||||
errorMessage = messages.error.genericFlashError(error);
|
||||
}
|
||||
|
||||
return errorMessage;
|
||||
} finally {
|
||||
availableDrives.setDrives([]);
|
||||
@@ -129,29 +132,10 @@ const flashImageToDrive = async (goToSuccess: () => void) => {
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get progress button label
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} progress button label
|
||||
*
|
||||
* @example
|
||||
* const label = FlashController.getProgressButtonLabel()
|
||||
*/
|
||||
const getProgressButtonLabel = () => {
|
||||
if (!flashState.isFlashing()) {
|
||||
return 'Flash!';
|
||||
}
|
||||
|
||||
// TODO: no any
|
||||
return progressStatus.fromFlashState(flashState.getFlashState() as any);
|
||||
};
|
||||
}
|
||||
|
||||
const formatSeconds = (totalSeconds: number) => {
|
||||
if (!totalSeconds && !_.isNumber(totalSeconds)) {
|
||||
if (typeof totalSeconds !== 'number' || !Number.isFinite(totalSeconds)) {
|
||||
return '';
|
||||
}
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
@@ -160,154 +144,206 @@ const formatSeconds = (totalSeconds: number) => {
|
||||
return `${minutes}m${seconds}s`;
|
||||
};
|
||||
|
||||
export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => {
|
||||
const state: any = flashState.getFlashState();
|
||||
const isFlashing = flashState.isFlashing();
|
||||
const flashErrorCode = flashState.getLastFlashErrorCode();
|
||||
interface FlashStepProps {
|
||||
shouldFlashStepBeDisabled: boolean;
|
||||
goToSuccess: () => void;
|
||||
isFlashing: boolean;
|
||||
style?: React.CSSProperties;
|
||||
// TODO: factorize
|
||||
step: 'decompressing' | 'flashing' | 'verifying';
|
||||
percentage: number;
|
||||
position: number;
|
||||
failed: number;
|
||||
speed?: number;
|
||||
eta?: number;
|
||||
width: string;
|
||||
}
|
||||
|
||||
const [warningMessages, setWarningMessages] = React.useState<string[]>([]);
|
||||
const [errorMessage, setErrorMessage] = React.useState('');
|
||||
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
|
||||
false,
|
||||
);
|
||||
export interface DriveWithWarnings extends constraints.DrivelistDrive {
|
||||
statuses: constraints.DriveStatus[];
|
||||
}
|
||||
|
||||
const handleWarningResponse = async (shouldContinue: boolean) => {
|
||||
setWarningMessages([]);
|
||||
interface FlashStepState {
|
||||
warningMessage: boolean;
|
||||
errorMessage: string;
|
||||
showDriveSelectorModal: boolean;
|
||||
systemDrives: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}
|
||||
|
||||
export class FlashStep extends React.PureComponent<
|
||||
FlashStepProps,
|
||||
FlashStepState
|
||||
> {
|
||||
constructor(props: FlashStepProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
warningMessage: false,
|
||||
errorMessage: '',
|
||||
showDriveSelectorModal: false,
|
||||
systemDrives: false,
|
||||
drivesWithWarnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleWarningResponse(shouldContinue: boolean) {
|
||||
this.setState({ warningMessage: false });
|
||||
if (!shouldContinue) {
|
||||
setShowDriveSelectorModal(true);
|
||||
this.setState({ showDriveSelectorModal: true });
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.isFlashing,
|
||||
this.props.goToSuccess,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
setErrorMessage(await flashImageToDrive(goToSuccess));
|
||||
};
|
||||
|
||||
const handleFlashErrorResponse = (shouldRetry: boolean) => {
|
||||
setErrorMessage('');
|
||||
private handleFlashErrorResponse(shouldRetry: boolean) {
|
||||
this.setState({ errorMessage: '' });
|
||||
flashState.resetState();
|
||||
if (shouldRetry) {
|
||||
analytics.logEvent('Restart after failure', {
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
analytics.logEvent('Restart after failure');
|
||||
} else {
|
||||
selection.clear();
|
||||
}
|
||||
};
|
||||
|
||||
const tryFlash = async () => {
|
||||
const devices = selection.getSelectedDevices();
|
||||
const image = selection.getImage();
|
||||
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
|
||||
return _.includes(devices, drive.device);
|
||||
});
|
||||
}
|
||||
|
||||
private hasListWarnings(drives: any[]) {
|
||||
if (drives.length === 0 || flashState.isFlashing()) {
|
||||
return;
|
||||
}
|
||||
return drives.filter((drive) => drive.isSystem).length > 0;
|
||||
}
|
||||
|
||||
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(
|
||||
drives,
|
||||
image,
|
||||
);
|
||||
if (hasDangerStatus) {
|
||||
setWarningMessages(getWarningMessages(drives, image));
|
||||
private async tryFlash() {
|
||||
const drives = selection.getSelectedDrives().map((drive) => {
|
||||
return {
|
||||
...drive,
|
||||
statuses: constraints.getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
};
|
||||
});
|
||||
if (drives.length === 0 || this.props.isFlashing) {
|
||||
return;
|
||||
}
|
||||
const hasDangerStatus = drives.some((drive) => drive.statuses.length > 0);
|
||||
if (hasDangerStatus) {
|
||||
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,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
setErrorMessage(await flashImageToDrive(goToSuccess));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="box text-center">
|
||||
<div className="center-block">
|
||||
<SVGIcon
|
||||
paths={['../../assets/flash.svg']}
|
||||
disabled={shouldFlashStepBeDisabled}
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<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',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-vertical-large">
|
||||
<ProgressButton
|
||||
striped={state.type === 'verifying'}
|
||||
active={isFlashing}
|
||||
percentage={state.percentage}
|
||||
label={getProgressButtonLabel()}
|
||||
disabled={Boolean(flashErrorCode) || shouldFlashStepBeDisabled}
|
||||
callback={tryFlash}
|
||||
></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()}
|
||||
/>
|
||||
|
||||
{isFlashing && (
|
||||
<button
|
||||
className="button button-link button-abort-write"
|
||||
onClick={imageWriter.cancel}
|
||||
>
|
||||
<span className="glyphicon glyphicon-remove-sign"></span>
|
||||
</button>
|
||||
)}
|
||||
{!_.isNil(state.speed) && state.percentage !== COMPLETED_PERCENTAGE && (
|
||||
<p className="step-footer step-footer-split">
|
||||
{Boolean(state.speed) && (
|
||||
<span>{`${state.speed.toFixed(SPEED_PRECISION)} MB/s`}</span>
|
||||
)}
|
||||
{!_.isNil(state.eta) && (
|
||||
<span>{`ETA: ${formatSeconds(state.eta)}`}</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{!_.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(state.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">{state.failed}</span>
|
||||
<span className="target-status-message">
|
||||
{messages.progress.failed(state.failed)}{' '}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
{warningMessages && warningMessages.length > 0 && (
|
||||
<Modal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => handleWarningResponse(false)}
|
||||
done={() => handleWarningResponse(true)}
|
||||
cancelButtonProps={{
|
||||
children: 'Change',
|
||||
}}
|
||||
action={'Continue'}
|
||||
primaryButtonProps={{ primary: false, warning: true }}
|
||||
>
|
||||
{_.map(warningMessages, (message, key) => (
|
||||
<Txt key={key} whitespace="pre-line" mt={2}>
|
||||
{message}
|
||||
{this.state.warningMessage && (
|
||||
<DriveStatusWarningModal
|
||||
done={() => this.handleWarningResponse(true)}
|
||||
cancel={() => this.handleWarningResponse(false)}
|
||||
isSystem={this.state.systemDrives}
|
||||
drivesWithWarnings={this.state.drivesWithWarnings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.errorMessage && (
|
||||
<SmallModal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => this.handleFlashErrorResponse(false)}
|
||||
done={() => this.handleFlashErrorResponse(true)}
|
||||
action={'Retry'}
|
||||
>
|
||||
<Txt>
|
||||
{this.state.errorMessage.split('\n').map((message, key) => (
|
||||
<p key={key}>{message}</p>
|
||||
))}
|
||||
</Txt>
|
||||
))}
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<Modal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => handleFlashErrorResponse(false)}
|
||||
done={() => handleFlashErrorResponse(true)}
|
||||
action={'Retry'}
|
||||
>
|
||||
<Txt>{errorMessage}</Txt>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{showDriveSelectorModal && (
|
||||
<DriveSelectorModal
|
||||
close={() => setShowDriveSelectorModal(false)}
|
||||
></DriveSelectorModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
</SmallModal>
|
||||
)}
|
||||
{this.state.showDriveSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
write={true}
|
||||
cancel={() => this.setState({ showDriveSelectorModal: false })}
|
||||
done={(modalTargets) => {
|
||||
selectAllTargets(modalTargets);
|
||||
this.setState({ showDriveSelectorModal: false });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -14,39 +14,49 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { faCog, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import { Button } from 'rendition';
|
||||
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 { FeaturedProject } from '../../components/featured-project/featured-project';
|
||||
import FinishPage from '../../components/finish/finish';
|
||||
import { ImageSelector } from '../../components/image-selector/image-selector';
|
||||
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||
import { SettingsModal } from '../../components/settings/settings';
|
||||
import { SVGIcon } from '../../components/svg-icon/svg-icon';
|
||||
import {
|
||||
SourceMetadata,
|
||||
SourceSelector,
|
||||
} 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';
|
||||
import { observe } from '../../models/store';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { ThemedProvider } from '../../styled-components';
|
||||
import { colors } from '../../theme';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import {
|
||||
IconButton as BaseIcon,
|
||||
ThemedProvider,
|
||||
} from '../../styled-components';
|
||||
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import {
|
||||
TargetSelector,
|
||||
getDriveListLabel,
|
||||
} from '../../components/target-selector/target-selector';
|
||||
import { FlashStep } from './Flash';
|
||||
|
||||
import { DriveSelector } from './DriveSelector';
|
||||
import { Flash } from './Flash';
|
||||
import EtcherSvg from '../../../assets/etcher.svg';
|
||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||
|
||||
const Icon = styled(BaseIcon)`
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
function getDrivesTitle() {
|
||||
const drives = selectionState.getSelectedDrives();
|
||||
|
||||
if (drives.length === 1) {
|
||||
// @ts-ignore
|
||||
return drives[0].description || 'Untitled Device';
|
||||
}
|
||||
|
||||
@@ -57,30 +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;
|
||||
featuredProjectURL?: string;
|
||||
}
|
||||
|
||||
export class MainPage extends React.Component<
|
||||
@@ -98,40 +132,160 @@ export class MainPage extends React.Component<
|
||||
}
|
||||
|
||||
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() });
|
||||
}
|
||||
|
||||
public render() {
|
||||
private renderMain() {
|
||||
const state = flashState.getFlashState();
|
||||
const shouldDriveStepBeDisabled = !this.state.hasImage;
|
||||
const shouldFlashStepBeDisabled =
|
||||
!this.state.hasImage || !this.state.hasDrive;
|
||||
const notFlashingOrSplitView =
|
||||
!this.state.isFlashing || !this.state.isWebviewShowing;
|
||||
return (
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
if (this.state.current === 'main') {
|
||||
return (
|
||||
<ThemedProvider style={{ height: '100%', width: '100%' }}>
|
||||
<header
|
||||
id="app-header"
|
||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
||||
<Flex
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '13px 14px',
|
||||
textAlign: 'center',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '36.2vw',
|
||||
height: '100vh',
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
<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={{
|
||||
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',
|
||||
}}
|
||||
@@ -139,123 +293,50 @@ export class MainPage extends React.Component<
|
||||
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
|
||||
}
|
||||
tabIndex={100}
|
||||
>
|
||||
<SVGIcon
|
||||
paths={['../../assets/etcher.svg']}
|
||||
width="123px"
|
||||
height="22px"
|
||||
/>
|
||||
</span>
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<span
|
||||
<Flex width="100%" alignItems="center" justifyContent="flex-end">
|
||||
<Icon
|
||||
icon={<CogSvg height="1em" fill="currentColor" />}
|
||||
plain
|
||||
tabIndex={5}
|
||||
onClick={() => this.setState({ hideSettings: false })}
|
||||
style={{
|
||||
float: 'right',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<FontAwesomeIcon icon={faCog} />}
|
||||
color={colors.secondary.background}
|
||||
fontSize={24}
|
||||
style={{ width: '30px' }}
|
||||
plain
|
||||
onClick={() => this.setState({ hideSettings: false })}
|
||||
tabIndex={5}
|
||||
/>
|
||||
{!settings.get('disableExternalLinks') && (
|
||||
<Button
|
||||
icon={<FontAwesomeIcon icon={faQuestionCircle} />}
|
||||
color={colors.secondary.background}
|
||||
fontSize={24}
|
||||
style={{ width: '30px' }}
|
||||
plain
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
selectionState.getImageSupportUrl() ||
|
||||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
|
||||
)
|
||||
}
|
||||
tabIndex={5}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</header>
|
||||
{this.state.hideSettings ? null : (
|
||||
<SettingsModal
|
||||
toggleModal={(value: boolean) => {
|
||||
this.setState({ hideSettings: !value });
|
||||
// Make touch events click instead of dragging
|
||||
WebkitAppRegion: 'no-drag',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="page-main row around-xs"
|
||||
style={{ margin: '110px 50px' }}
|
||||
>
|
||||
<div className="col-xs">
|
||||
<ImageSelector flashing={this.state.isFlashing} />
|
||||
</div>
|
||||
|
||||
<div className="col-xs">
|
||||
<DriveSelector
|
||||
webviewShowing={this.state.isWebviewShowing}
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
nextStepDisabled={shouldFlashStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
{!settings.getSync('disableExternalLinks') && (
|
||||
<Icon
|
||||
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</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">
|
||||
<Flash
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ThemedProvider>
|
||||
);
|
||||
} else if (this.state.current === 'success') {
|
||||
return (
|
||||
<div className="section-loader isFinish">
|
||||
<FinishPage goToMain={() => this.setState({ current: 'main' })} />
|
||||
<SafeWebview src="https://www.balena.io/etcher/success-banner/" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{this.state.hideSettings ? null : (
|
||||
<SettingsModal
|
||||
toggleModal={(value: boolean) => {
|
||||
this.setState({ hideSettings: !value });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{this.state.current === 'main'
|
||||
? this.renderMain()
|
||||
: this.renderSuccess()}
|
||||
</ThemedProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,201 +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 .step-selection-text {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
color: $palette-theme-dark-foreground;
|
||||
}
|
||||
|
||||
.page-main .text-disabled > span {
|
||||
color: $palette-theme-dark-disabled-foreground;
|
||||
}
|
||||
|
||||
.page-main .step-drive.text-warning {
|
||||
color: $palette-theme-warning-background;
|
||||
}
|
||||
|
||||
.page-main .relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-main .button-abort-write {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
right: -17px;
|
||||
top: 30%;
|
||||
}
|
||||
|
||||
.button-brick {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.page-main .step-tooltip {
|
||||
display: block;
|
||||
margin: -5px auto -20px;
|
||||
color: $palette-theme-dark-disabled-foreground;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.page-main .step-footer {
|
||||
width: 100%;
|
||||
color: $palette-theme-dark-disabled-foreground;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.page-main p.step-footer {
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.page-main .step-footer-split {
|
||||
position: absolute;
|
||||
top: 39px;
|
||||
left: 28px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: $btn-min-width;
|
||||
}
|
||||
|
||||
.page-main .button.step-footer {
|
||||
font-size: 16px;
|
||||
color: $palette-theme-primary-background;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
font-weight: 300;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.page-main .step-drive.glyphicon {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.page-main div.step-fill,
|
||||
.page-main span.step-fill {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.page-main .step-drive.step-list {
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $palette-theme-dark-disabled-foreground;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.page-main .step-size {
|
||||
color: $palette-theme-dark-disabled-foreground;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
height: 21px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-main .step-list {
|
||||
height: 80px;
|
||||
margin: 15px;
|
||||
overflow-y: auto;
|
||||
color: $palette-theme-dark-disabled-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;
|
||||
margin-bottom: 9px;
|
||||
|
||||
> .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;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.space-vertical-large {
|
||||
position: relative;
|
||||
}
|
10
lib/gui/app/renderer.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import { main } from './app';
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./app', () => {
|
||||
main();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
@@ -1,24 +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.
|
||||
*/
|
||||
|
||||
.badge {
|
||||
border: 2px solid;
|
||||
border-radius: 50%;
|
||||
padding: 7px 10px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
letter-spacing: 0;
|
||||
}
|
@@ -1,99 +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.
|
||||
*/
|
||||
|
||||
.button {
|
||||
@extend .btn;
|
||||
|
||||
padding: 10px;
|
||||
padding-top: 11px;
|
||||
|
||||
border-radius: 24px;
|
||||
border: 0;
|
||||
|
||||
letter-spacing: .5px;
|
||||
outline: none;
|
||||
|
||||
position: relative;
|
||||
|
||||
> .glyphicon {
|
||||
top: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&.button-primary{
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
@extend .button-no-hover;
|
||||
background-color: $palette-theme-dark-disabled-background;
|
||||
color: $palette-theme-dark-disabled-foreground;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.button-link {
|
||||
@extend .btn-link;
|
||||
}
|
||||
|
||||
.button-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-no-hover {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Create map from Bootstrap `.btn` type styles
|
||||
// since its not possible to perform variable
|
||||
// interpolation (e.g: `$btn-${type}-bg`).
|
||||
// See https://github.com/sass/sass/issues/132
|
||||
$button-types-styles: (
|
||||
default: (
|
||||
bg: $palette-theme-default-background,
|
||||
color: $palette-theme-default-foreground
|
||||
),
|
||||
primary: (
|
||||
bg: $palette-theme-primary-background,
|
||||
color: $palette-theme-primary-foreground
|
||||
),
|
||||
danger: (
|
||||
bg: $palette-theme-danger-background,
|
||||
color: $palette-theme-danger-foreground
|
||||
),
|
||||
warning: (
|
||||
bg: $palette-theme-warning-background,
|
||||
color: $palette-theme-danger-foreground
|
||||
)
|
||||
);
|
||||
|
||||
@each $style in map-keys($button-types-styles) {
|
||||
$button-styles: map-get($button-types-styles, $style);
|
||||
|
||||
.button-#{$style} {
|
||||
background-color: map-get($button-styles, "bg");
|
||||
color: map-get($button-styles, "color");
|
||||
}
|
||||
|
||||
.button-#{$style}:focus,
|
||||
.button-#{$style}:hover {
|
||||
background-color: darken(map-get($button-styles, "bg"), 10%);
|
||||
color: map-get($button-styles, "color");
|
||||
}
|
||||
}
|
@@ -1,21 +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.
|
||||
*/
|
||||
|
||||
.caption {
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
margin-bottom: 0;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -1,182 +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/bootstrap-sass/assets/stylesheets/bootstrap";
|
||||
@import "./modules/theme";
|
||||
@import "./modules/bootstrap";
|
||||
@import "./modules/space";
|
||||
@import "./components/label";
|
||||
@import "./components/badge";
|
||||
@import "./components/caption";
|
||||
@import "./components/button";
|
||||
@import "./components/tick";
|
||||
@import "../components/drive-selector/styles/drive-selector";
|
||||
@import "../pages/main/styles/main";
|
||||
@import "../pages/finish/styles/finish";
|
||||
|
||||
$fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts";
|
||||
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fontawesome";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-solid";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
src: url('Nunito-Regular.eot');
|
||||
src: url('./fonts/Nunito-Regular.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/Nunito-Regular.woff2') format('woff2'),
|
||||
url('./fonts/Nunito-Regular.woff') format('woff'),
|
||||
url('./fonts/Nunito-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
src: url('Nunito-Bold.eot');
|
||||
src: url('./fonts/Nunito-Bold.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/Nunito-Bold.woff2') format('woff2'),
|
||||
url('./fonts/Nunito-Bold.woff') format('woff'),
|
||||
url('./fonts/Nunito-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
src: url('Nunito-Light.eot');
|
||||
src: url('./fonts/Nunito-Light.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/Nunito-Light.woff2') format('woff2'),
|
||||
url('./fonts/Nunito-Light.woff') format('woff'),
|
||||
url('./fonts/Nunito-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'CircularStd';
|
||||
src: url('./fonts/CircularStd-Bold.eot');
|
||||
src: url('./fonts/CircularStd-Bold.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/CircularStd-Bold.woff2') format('woff2'),
|
||||
url('./fonts/CircularStd-Bold.woff') format('woff'),
|
||||
url('./fonts/CircularStd-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'CircularStd';
|
||||
src: url('./fonts/CircularStd-Book.eot');
|
||||
src: url('./fonts/CircularStd-Book.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/CircularStd-Book.woff2') format('woff2'),
|
||||
url('./fonts/CircularStd-Book.woff') format('woff'),
|
||||
url('./fonts/CircularStd-Book.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'CircularStd';
|
||||
src: url('./fonts/CircularStd-Medium.eot');
|
||||
src: url('./fonts/CircularStd-Medium.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/CircularStd-Medium.woff2') format('woff2'),
|
||||
url('./fonts/CircularStd-Medium.woff') format('woff'),
|
||||
url('./fonts/CircularStd-Medium.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
.circular {
|
||||
font-family: 'CircularStd';
|
||||
font-weight: 500;
|
||||
}
|
||||
.nunito {
|
||||
font-family: 'Nunito';
|
||||
}
|
||||
|
||||
body {
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: 'CircularStd';
|
||||
|
||||
> header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
> 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;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
margin: 20px 50px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
@@ -1,42 +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;
|
||||
}
|
||||
|
||||
// Fix slight checkbox vertical alignment issue
|
||||
.checkbox input[type="checkbox"] {
|
||||
position: initial;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
[uib-tooltip] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
@@ -14,76 +14,78 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Button, Flex, 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 theme = {
|
||||
// TODO: Standardize how the colors are specified to match with rendition's format.
|
||||
customColors: colors,
|
||||
button: {
|
||||
border: {
|
||||
width: '0',
|
||||
radius: '24px',
|
||||
},
|
||||
disabled: {
|
||||
opacity: 1,
|
||||
},
|
||||
extend: () => `
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
|
||||
&:disabled {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
};
|
||||
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 StepButton = (props: any) => (
|
||||
<BaseButton primary {...props}></BaseButton>
|
||||
);
|
||||
export const IconButton = styled((props) => <Button plain {...props} />)`
|
||||
&&& {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
|
||||
export const ChangeButton = styled(BaseButton)`
|
||||
color: ${colors.primary.background};
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
&:enabled {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: #8f9297;
|
||||
> svg {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
${space}
|
||||
`;
|
||||
|
||||
export const StepButton = styled((props: ButtonProps) => (
|
||||
<BaseButton {...props}></BaseButton>
|
||||
))`
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const ChangeButton = styled(Button)`
|
||||
&& {
|
||||
border-radius: 24px;
|
||||
color: ${colors.primary.background};
|
||||
padding: 0;
|
||||
height: 18px;
|
||||
font-size: 14px;
|
||||
|
||||
&:enabled {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: #8f9297;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const StepNameButton = styled(BaseButton)`
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
color: ${colors.dark.foreground};
|
||||
|
||||
&:enabled {
|
||||
@@ -94,20 +96,230 @@ export const StepNameButton = styled(BaseButton)`
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const StepSelection = styled(Flex)`
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
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} />;
|
||||
};
|
||||
|
@@ -14,6 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { Theme } from 'rendition';
|
||||
|
||||
export const colors = {
|
||||
dark: {
|
||||
foreground: '#fff',
|
||||
@@ -44,11 +47,12 @@ export const colors = {
|
||||
},
|
||||
primary: {
|
||||
foreground: '#fff',
|
||||
background: '#2297de',
|
||||
background: '#00aeef',
|
||||
},
|
||||
secondary: {
|
||||
foreground: '#000',
|
||||
background: '#ddd',
|
||||
main: '#fff',
|
||||
},
|
||||
warning: {
|
||||
foreground: '#fff',
|
||||
@@ -63,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;
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 5.5 KiB |
18
lib/gui/assets/src.svg
Normal 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
@@ -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 |
@@ -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 |