Compare commits
583 Commits
v1.5.100
...
aethernet/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec7e0b745e | ||
|
|
d5ba1ea5e1 | ||
|
|
54d3636a22 | ||
|
|
45f6ee667d | ||
|
|
d25eda9a7d | ||
|
|
7420283249 | ||
|
|
453952440f | ||
|
|
2475d576c7 | ||
|
|
8cd6da1260 | ||
|
|
82dd4fc1d1 | ||
|
|
33fe4b2c1a | ||
|
|
b1c1188107 | ||
|
|
63b45aefae | ||
|
|
f79cb0fac5 | ||
|
|
ec42892c7c | ||
|
|
371716fe6a | ||
|
|
d5fb6bec15 | ||
|
|
c5e7bf26d7 | ||
|
|
e3072ac416 | ||
|
|
dfaf06e4cf | ||
|
|
6e24d25576 | ||
|
|
b59b171e43 | ||
|
|
28726584c2 | ||
|
|
00b151311a | ||
|
|
36c813714b | ||
|
|
2ae6764dd9 | ||
|
|
debefc9652 | ||
|
|
b068b847c7 | ||
|
|
6c410c07ce | ||
|
|
c01206c1f3 | ||
|
|
2e85fb45de | ||
|
|
67513e384d | ||
|
|
828dafa493 | ||
|
|
5c5a761222 | ||
|
|
fab10e5fc5 | ||
|
|
797345fc1c | ||
|
|
a0388a43c3 | ||
|
|
f5b0a3023b | ||
|
|
dc1d7bd1fd | ||
|
|
9d674321b6 | ||
|
|
f9c8378d6a | ||
|
|
65da751a52 | ||
|
|
72142be0de | ||
|
|
11cea7c926 | ||
|
|
8d46ee4c22 | ||
|
|
d63c09e2c2 | ||
|
|
c9e9d7d109 | ||
|
|
2412d20eb4 | ||
|
|
7f90d23a12 | ||
|
|
b9a82be29b | ||
|
|
638673ba5e | ||
|
|
898fe4f216 | ||
|
|
7e805662d1 | ||
|
|
baf59c73ac | ||
|
|
38ad9c97c6 | ||
|
|
8fc574f059 | ||
|
|
78b0f00e88 | ||
|
|
0f10f2d483 | ||
|
|
eb5f5bbb9e | ||
|
|
67d26ff790 | ||
|
|
17f2008d88 | ||
|
|
db1bf7e488 | ||
|
|
4b786b8a9f | ||
|
|
fdfa0d3258 | ||
|
|
757aa77d89 | ||
|
|
d70ea06565 | ||
|
|
f2ebd10053 | ||
|
|
cd67b442c9 | ||
|
|
852c83c4fb | ||
|
|
e3b2ee3b83 | ||
|
|
927a026b86 | ||
|
|
c851e1d54f | ||
|
|
e6fdca171f | ||
|
|
c9cfb87733 | ||
|
|
b0b7c53294 | ||
|
|
e8dc6579fe | ||
|
|
f0747abe3f | ||
|
|
32fab87340 | ||
|
|
adcd8e0325 | ||
|
|
7b5808eb2b | ||
|
|
a8f7422cf5 | ||
|
|
5ae9a26361 | ||
|
|
cf1fdb8c5f | ||
|
|
bf7ebde100 | ||
|
|
88c5fa5035 | ||
|
|
887b0dd538 | ||
|
|
364d1db56a | ||
|
|
c431222909 | ||
|
|
55a0f68b97 | ||
|
|
af2563dfc2 | ||
|
|
33f8851083 | ||
|
|
fe1f19b9fa | ||
|
|
871cf3ec0a | ||
|
|
686a5837b6 | ||
|
|
23f2dd5ce5 | ||
|
|
d5d39b395b | ||
|
|
2acad790d3 | ||
|
|
30133306d6 | ||
|
|
04a62f2ad8 | ||
|
|
17858a7d72 | ||
|
|
620307568f | ||
|
|
a349c5d9ac | ||
|
|
0d740ad12d | ||
|
|
85a3f28869 | ||
|
|
dbd5397405 | ||
|
|
85c183b9ef | ||
|
|
0d0af1d1dd | ||
|
|
ad423fc187 | ||
|
|
d8b2a7a236 | ||
|
|
13ec8cbe98 | ||
|
|
a7cae23612 | ||
|
|
86bb093f3d | ||
|
|
997e1eb2f2 | ||
|
|
34cc8b8933 | ||
|
|
f26b074811 | ||
|
|
adaa07b4b0 | ||
|
|
96f4569342 | ||
|
|
be190c6c80 | ||
|
|
809617a82d | ||
|
|
df02732002 | ||
|
|
d35f3c3049 | ||
|
|
8b047e3b14 | ||
|
|
fa41d21e27 | ||
|
|
54e6c5e2c1 | ||
|
|
43fc3dd7eb | ||
|
|
12a1340c8e | ||
|
|
cf8b5790a1 | ||
|
|
659d85a833 | ||
|
|
96c44d31c9 | ||
|
|
ba812b4f64 | ||
|
|
4087258fbd | ||
|
|
955be13129 | ||
|
|
32011c0dea | ||
|
|
b68325c71c | ||
|
|
84bce86fce | ||
|
|
d68eab1dda | ||
|
|
09cf014d14 | ||
|
|
d5bab5805f | ||
|
|
b5ab500a14 | ||
|
|
49253d37c9 | ||
|
|
97cf3b25ad | ||
|
|
99862b95a5 | ||
|
|
8b765d58e5 | ||
|
|
8f566e45b8 | ||
|
|
b8af86e30c | ||
|
|
784f193b6d | ||
|
|
3967adb1b5 | ||
|
|
0667d1110f | ||
|
|
61dd22bdf3 | ||
|
|
24eb8b05b0 | ||
|
|
6991a4950b | ||
|
|
bb169cf674 | ||
|
|
e5d0d2e262 | ||
|
|
72b4d4f4fa | ||
|
|
9b2f2eb4c3 | ||
|
|
ce52ef95a9 | ||
|
|
aa3756ad17 | ||
|
|
73081e726d | ||
|
|
d53dc4149b | ||
|
|
0d5bb4935f | ||
|
|
14aeb0060b | ||
|
|
239726f3ce | ||
|
|
4ed3002716 | ||
|
|
7286fba240 | ||
|
|
895c306fb7 | ||
|
|
f3844d56e2 | ||
|
|
540dc3150a | ||
|
|
035c8dfec3 | ||
|
|
03d6a011db | ||
|
|
27f64650f9 | ||
|
|
ccca009972 | ||
|
|
57a6ceff0e | ||
|
|
30c4baa58b | ||
|
|
a930d77064 | ||
|
|
0d1cfffa5c | ||
|
|
3c7422764c | ||
|
|
55176b9f8f | ||
|
|
156b9314b5 | ||
|
|
76d22280dc | ||
|
|
e4251a3862 | ||
|
|
831339bd2c | ||
|
|
952ea80e15 | ||
|
|
813c497e4b | ||
|
|
1b5b647135 | ||
|
|
7de99003ca | ||
|
|
e09bdd734b | ||
|
|
306e087ec6 | ||
|
|
c6b0178a87 | ||
|
|
4e581ea1ce | ||
|
|
26dc2d19e5 | ||
|
|
b99282acfb | ||
|
|
4e48724d0c | ||
|
|
448ce141d5 | ||
|
|
695f287190 | ||
|
|
4de3271e15 | ||
|
|
77b33b127d | ||
|
|
9cd13ba381 | ||
|
|
9df23c8a3f | ||
|
|
e3618b939e | ||
|
|
6a39f5869a | ||
|
|
fd472efadc | ||
|
|
7e2c2eae63 | ||
|
|
5266571ca4 | ||
|
|
797868c474 | ||
|
|
2c2a5c7c2b | ||
|
|
9e536d5337 | ||
|
|
860e680dd9 | ||
|
|
7bb52aa170 | ||
|
|
1c370f9100 | ||
|
|
ec7c772d0b | ||
|
|
cc0285a77d | ||
|
|
256d3550d1 | ||
|
|
db3a5f3b0a | ||
|
|
0e58edf113 | ||
|
|
db136926a9 | ||
|
|
d84e7211be | ||
|
|
8357cc19d2 | ||
|
|
2752b9fa95 | ||
|
|
0214be4953 | ||
|
|
a4f944e795 | ||
|
|
cd2ebf15fc | ||
|
|
7a7ea374e9 | ||
|
|
330df325f9 | ||
|
|
2fc0882b2e | ||
|
|
4dd779e010 | ||
|
|
3dc54405fe | ||
|
|
3f1aa5bac3 | ||
|
|
8f52fdb900 | ||
|
|
1b93891ed8 | ||
|
|
33adc8ecf8 | ||
|
|
0455f7ea58 | ||
|
|
ea5a167f4f | ||
|
|
8a1c4a4cc8 | ||
|
|
bd8bc81713 | ||
|
|
98a5ddf58a | ||
|
|
6223dbc541 | ||
|
|
7c56621c57 | ||
|
|
a61aa8e2be | ||
|
|
7df4f9615b | ||
|
|
5742452fdf | ||
|
|
fe09f9f862 | ||
|
|
3a4687ea0f | ||
|
|
db6490fb1b | ||
|
|
1642297101 | ||
|
|
5ecd223cfc | ||
|
|
306e40fd7b | ||
|
|
b58249b9c8 | ||
|
|
b23b4b34d0 | ||
|
|
73bc921713 | ||
|
|
f356e4c303 | ||
|
|
9888167f2e | ||
|
|
4561690478 | ||
|
|
576113febf | ||
|
|
cc139bf750 | ||
|
|
ae91958c06 | ||
|
|
33dea6267f | ||
|
|
c9a8bca96f | ||
|
|
8af376e608 | ||
|
|
9ab307df4f | ||
|
|
e8a716f8bb | ||
|
|
a40e64f6cd | ||
|
|
2e53feb38c | ||
|
|
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 |
@@ -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
|
||||
|
||||
8
.gitattributes
vendored
@@ -1,3 +1,6 @@
|
||||
# default
|
||||
* text
|
||||
|
||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
@@ -27,6 +30,7 @@ Makefile text
|
||||
*.yml text
|
||||
*.patch text
|
||||
*.txt text
|
||||
*.tpl text
|
||||
CODEOWNERS text
|
||||
*.plist text
|
||||
|
||||
@@ -58,3 +62,7 @@ CODEOWNERS text
|
||||
*.ttf binary diff=hex
|
||||
xz-without-extension binary diff=hex
|
||||
wmic-output.txt binary diff=hex
|
||||
|
||||
# gitsecret
|
||||
*.secret binary
|
||||
.gitsecret/** binary
|
||||
|
||||
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. -->
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
221
.github/actions/publish/action.yml
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
---
|
||||
name: package and publish GitHub (draft) release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
XCODE_APP_LOADER_EMAIL:
|
||||
type: string
|
||||
default: "accounts+apple@balena.io"
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: "14.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Extract custom source artifact
|
||||
shell: pwsh
|
||||
working-directory: .
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Install yq
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: choco install yq
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
# FIXME: resinci-deploy is not actively maintained
|
||||
# https://github.com/product-os/resinci-deploy
|
||||
- name: Checkout resinci-deploy
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: product-os/resinci-deploy
|
||||
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
||||
path: resinci-deploy
|
||||
|
||||
- name: Build and install resinci-deploy
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
rm -rf ../resinci-deploy && mv resinci-deploy ..
|
||||
|
||||
pushd ../resinci-deploy && npm ci && npm link && popd
|
||||
|
||||
if [[ $runner_os =~ linux|macos ]]; then
|
||||
chmod +x "$(dirname "$(which node)")/resinci-deploy" && which resinci-deploy
|
||||
fi
|
||||
|
||||
# Upload sourcemaps to sentry
|
||||
- name: Generate Sentry DSN
|
||||
id: sentry
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
branch="$(echo '${{ github.event.pull_request.head.ref }}' | sed 's/[^[:alnum:]]/-/g')"
|
||||
|
||||
stdout="$(resinci-deploy store sentry \
|
||||
--branch="${branch}" \
|
||||
--name="$(jq -r '.name' package.json)" \
|
||||
--team="$(yq e '.sentry.team' repo.yml)" \
|
||||
--org="$(yq e '.sentry.org' repo.yml)" \
|
||||
--type="$(yq e '.sentry.type' repo.yml)")"
|
||||
|
||||
echo "dsn=$(echo "${stdout}" | tail -n 1)" >> $GITHUB_OUTPUT
|
||||
|
||||
env:
|
||||
SENTRY_TOKEN: ${{ fromJSON(inputs.secrets).SENTRY_AUTH_TOKEN }}
|
||||
|
||||
# https://www.electron.build/code-signing.html
|
||||
# https://github.com/Apple-Actions/import-codesign-certs
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
|
||||
- name: Import Windows code signing certificate
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: |
|
||||
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
|
||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
||||
|
||||
Import-PfxCertificate `
|
||||
-FilePath ${{ runner.temp }}/certificate.pfx `
|
||||
-CertStoreLocation Cert:\CurrentUser\My `
|
||||
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
|
||||
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.pfx
|
||||
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
|
||||
# ... or refactor (e.g.) https://github.com/samuelmeuli/action-electron-builder
|
||||
# https://github.com/product-os/scripts/tree/master/electron
|
||||
# https://github.com/product-os/scripts/tree/master/shared
|
||||
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
|
||||
- name: Package release
|
||||
id: package_release
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
ELECTRON_BUILDER_ARCHITECTURE="${runner_arch}"
|
||||
APPLICATION_VERSION="$(jq -r '.version' package.json)"
|
||||
ARCHITECTURE_FLAGS="--${ELECTRON_BUILDER_ARCHITECTURE}"
|
||||
|
||||
if [[ $runner_os =~ linux ]]; then
|
||||
ELECTRON_BUILDER_OS='--linux'
|
||||
TARGETS="$(yq e .linux.target[] electron-builder.yml)"
|
||||
|
||||
elif [[ $runner_os =~ darwin|macos|osx ]]; then
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
CSC_KEYCHAIN=signing_temp
|
||||
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
ELECTRON_BUILDER_OS='--mac'
|
||||
TARGETS="$(yq e .mac.target[] electron-builder.yml)"
|
||||
|
||||
elif [[ $runner_os =~ windows|win ]]; then
|
||||
ARCHITECTURE_FLAGS="--ia32 ${ARCHITECTURE_FLAGS}"
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
CSC_LINK=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
ELECTRON_BUILDER_OS='--win'
|
||||
TARGETS="$(yq e .win.target[] electron-builder.yml)"
|
||||
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm link electron-builder
|
||||
|
||||
for target in ${TARGETS}; do
|
||||
electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \
|
||||
--c.extraMetadata.analytics.sentry.token='${{ steps.sentry.outputs.dsn }}' \
|
||||
--c.extraMetadata.analytics.mixpanel.token='balena-etcher' \
|
||||
--c.extraMetadata.packageType="${target}"
|
||||
|
||||
find dist -type f -maxdepth 1
|
||||
done
|
||||
|
||||
echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
env:
|
||||
# Apple notarization (afterSignHook.js)
|
||||
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
|
||||
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
|
||||
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
|
||||
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
|
||||
# https://www.electron.build/auto-update.html#staged-rollouts
|
||||
- name: Configure staged rollout(s)
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
percentage="$(cat < repo.yml | yq e .triggerNotification.stagingPercentage)"
|
||||
|
||||
find dist -type f -maxdepth 1 \
|
||||
-name "latest*.yml" \
|
||||
-exec yq -i e .version=\"${{ steps.package_release.outputs.version }}\" {} \;
|
||||
|
||||
find dist -type f -maxdepth 1 \
|
||||
-name "latest*.yml" \
|
||||
-exec yq -i e .stagingPercentage=\"$percentage\" {} \;
|
||||
|
||||
- name: Upload sourcemap to Sentry
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
VERSION=${{ steps.package_release.outputs.version }} npm run uploadSourcemap
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ fromJSON(inputs.secrets).SENTRY_AUTH_TOKEN }}
|
||||
npm_config_SENTRY_ORG: balenaEtcher
|
||||
npm_config_SENTRY_PROJECT: balenaetcher
|
||||
npm_config_SENTRY_VERSION: ${{ steps.package_release.outputs.version }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
|
||||
path: dist
|
||||
retention-days: 1
|
||||
58
.github/actions/test/action.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: test release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: "14.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Test release
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
npm run flowzone-preinstall-${runner_os}
|
||||
npm ci
|
||||
npm run build
|
||||
npm run test-${runner_os}
|
||||
|
||||
env:
|
||||
# https://www.electronjs.org/docs/latest/api/environment-variables
|
||||
ELECTRON_NO_ATTACH_CONSOLE: true
|
||||
|
||||
- name: Compress custom source
|
||||
shell: pwsh
|
||||
run: tar -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
retention-days: 1
|
||||
29
.github/workflows/flowzone.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Flowzone
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
# allow external contributions to use secrets within trusted code
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
flowzone:
|
||||
name: Flowzone
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
||||
# prevent duplicate workflows and only allow one `pull_request` or `pull_request_target` for
|
||||
# internal or external contributions respectively
|
||||
if: |
|
||||
(github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request') ||
|
||||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
|
||||
secrets: inherit
|
||||
with:
|
||||
tests_run_on: '["ubuntu-20.04","macos-latest","windows-2019"]'
|
||||
restrict_custom_actions: false
|
||||
github_prerelease: true
|
||||
repo_config: true
|
||||
repo_description: "Flash OS images to SD cards & USB drives, safely and easily."
|
||||
repo_homepage: https://etcher.io/
|
||||
repo_enable_wiki: true
|
||||
13
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Publish to WinGet
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest # action can only be run on windows
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v1
|
||||
with:
|
||||
identifier: Balena.Etcher
|
||||
installers-regex: 'balenaEtcher-Setup.*.exe$'
|
||||
token: ${{ secrets.WINGET_PAT }}
|
||||
10
.gitignore
vendored
@@ -47,3 +47,13 @@ node_modules
|
||||
# OSX files
|
||||
|
||||
.DS_Store
|
||||
|
||||
# VSCode files
|
||||
|
||||
.vscode
|
||||
.gitsecret/keys/random_seed
|
||||
!*.secret
|
||||
secrets/APPLE_SIGNING_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING_PASSWORD.txt
|
||||
secrets/XCODE_APP_LOADER_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING.pfx
|
||||
|
||||
BIN
.gitsecret/keys/pubring.kbx
Normal file
BIN
.gitsecret/keys/pubring.kbx~
Normal file
BIN
.gitsecret/keys/trustdb.gpg
Normal file
5
.gitsecret/paths/mapping.cfg
Normal file
@@ -0,0 +1,5 @@
|
||||
secrets/APPLE_SIGNING_PASSWORD.txt:5c9cfeb1ea5142b547bc842cc6e0b4a932641ae9811ee47abe2c3953f2a4de5d
|
||||
secrets/WINDOWS_SIGNING_PASSWORD.txt:852e431628494f2559793c39cf09c34e9406dd79bb15b90c9f88194020470568
|
||||
secrets/XCODE_APP_LOADER_PASSWORD.txt:005eb9a3c7035c77232973c9355468fc396b94e62783fb8e6dce16bce95b94a1
|
||||
secrets/WINDOWS_SIGNING.pfx:929f401db38733ffc41572539de7c0d938023af51ed06c205a72a71c1f815714
|
||||
secrets/APPLE_SIGNING.p12:61abf7b4ff2eec76ce889d71bcdd568b99a6a719b4947ac20f03966265b0946a
|
||||
@@ -1,74 +0,0 @@
|
||||
{
|
||||
"electron": {
|
||||
"npm_version": "6.14.5",
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
"libusb-1.0-0-dev",
|
||||
"libyaml-dev",
|
||||
"libgtk-3-0",
|
||||
"libatk-bridge2.0-0",
|
||||
"libdbus-1-3",
|
||||
"libgbm1",
|
||||
"libc6"
|
||||
]
|
||||
},
|
||||
"builder": {
|
||||
"appId": "io.balena.etcher",
|
||||
"copyright": "Copyright 2016-2020 Balena Ltd",
|
||||
"productName": "balenaEtcher",
|
||||
"nodeGypRebuild": true,
|
||||
"afterPack": "./afterPack.js",
|
||||
"asar": false,
|
||||
"files": [
|
||||
"generated",
|
||||
"lib/shared/catalina-sudo/sudo-askpass.osascript.js"
|
||||
],
|
||||
"beforeBuild": "./beforeBuild.js",
|
||||
"afterSign": "./afterSignHook.js",
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "entitlements.mac.plist",
|
||||
"entitlementsInherit": "entitlements.mac.plist"
|
||||
},
|
||||
"dmg": {
|
||||
"iconSize": 110,
|
||||
"contents": [
|
||||
{
|
||||
"x": 140,
|
||||
"y": 245
|
||||
},
|
||||
{
|
||||
"x": 415,
|
||||
"y": 245,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"width": 544,
|
||||
"height": 407
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
"packageCategory": "utils",
|
||||
"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
|
||||
|
||||
12860
.versionbot/CHANGELOG.yml
Normal file
1329
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).
|
||||
|
||||
71
Makefile
@@ -3,18 +3,12 @@
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
RESIN_SCRIPTS ?= ./scripts/resin
|
||||
export NPM_VERSION ?= 6.14.5
|
||||
export NPM_VERSION ?= 6.14.8
|
||||
S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||
|
||||
# This directory will be completely deleted by the `clean` rule
|
||||
BUILD_DIRECTORY ?= dist
|
||||
|
||||
# See http://stackoverflow.com/a/20763842/1641422
|
||||
BUILD_DIRECTORY_PARENT = $(dir $(BUILD_DIRECTORY))
|
||||
ifeq ($(wildcard $(BUILD_DIRECTORY_PARENT).),)
|
||||
$(error $(BUILD_DIRECTORY_PARENT) does not exist)
|
||||
endif
|
||||
|
||||
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
|
||||
|
||||
$(BUILD_DIRECTORY):
|
||||
@@ -23,9 +17,7 @@ $(BUILD_DIRECTORY):
|
||||
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
|
||||
# See https://stackoverflow.com/a/13468229/1641422
|
||||
SHELL := /bin/bash
|
||||
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Operating system and architecture detection
|
||||
@@ -74,6 +66,9 @@ else
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
HOST_ARCH = x64
|
||||
endif
|
||||
ifeq ($(shell uname -m),arm64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -93,12 +88,10 @@ TARGET_ARCH ?= $(HOST_ARCH)
|
||||
# ---------------------------------------------------------------------
|
||||
# Electron
|
||||
# ---------------------------------------------------------------------
|
||||
electron-develop: | $(BUILD_TEMPORARY_DIRECTORY)
|
||||
$(RESIN_SCRIPTS)/electron/install.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-s $(PLATFORM) \
|
||||
-m $(NPM_VERSION)
|
||||
electron-develop:
|
||||
git submodule update --init && \
|
||||
npm ci && \
|
||||
npm run webpack
|
||||
|
||||
electron-test:
|
||||
$(RESIN_SCRIPTS)/electron/test.sh \
|
||||
@@ -124,56 +117,20 @@ TARGETS = \
|
||||
help \
|
||||
info \
|
||||
lint \
|
||||
lint-ts \
|
||||
lint-sass \
|
||||
lint-cpp \
|
||||
lint-spell \
|
||||
test-spectron \
|
||||
test-gui \
|
||||
test \
|
||||
sanity-checks \
|
||||
clean \
|
||||
distclean \
|
||||
webpack \
|
||||
electron-develop \
|
||||
electron-test \
|
||||
electron-build
|
||||
|
||||
webpack:
|
||||
./node_modules/.bin/webpack
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
|
||||
lint-ts:
|
||||
balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
lint-sass:
|
||||
sass-lint -v lib/gui/app/scss/**/*.scss lib/gui/app/scss/*.scss
|
||||
|
||||
lint-cpp:
|
||||
cpplint --recursive src
|
||||
|
||||
lint-spell:
|
||||
codespell \
|
||||
--dictionary - \
|
||||
--dictionary dictionary.txt \
|
||||
--skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
|
||||
lib tests docs Makefile *.md LICENSE
|
||||
|
||||
lint: lint-ts lint-sass lint-cpp lint-spell
|
||||
|
||||
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register --require-main "tests/gui/allow-renderer-process-reuse.ts"
|
||||
|
||||
test-spectron:
|
||||
mocha $(MOCHA_OPTIONS) tests/spectron/runner.spec.ts
|
||||
|
||||
test-gui:
|
||||
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.ts
|
||||
|
||||
test-sdk:
|
||||
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.ts
|
||||
|
||||
test: test-gui test-sdk test-spectron
|
||||
test:
|
||||
npm run test
|
||||
|
||||
help:
|
||||
@echo "Available targets: $(TARGETS)"
|
||||
@@ -183,15 +140,11 @@ info:
|
||||
@echo "Host arch : $(HOST_ARCH)"
|
||||
@echo "Target arch : $(TARGET_ARCH)"
|
||||
|
||||
sanity-checks:
|
||||
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIRECTORY)
|
||||
|
||||
distclean: clean
|
||||
rm -rf node_modules
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
rm -rf generated
|
||||
rm -rf $(BUILD_TEMPORARY_DIRECTORY)
|
||||
|
||||
185
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,81 +30,118 @@ Note that Etcher will run on any platform officially supported by
|
||||
Refer to the [downloads page][etcher] for the latest pre-made
|
||||
installers for all supported operating systems.
|
||||
|
||||
## Packages
|
||||
|
||||
> [](https://cloudsmith.com) \
|
||||
Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com).
|
||||
Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that
|
||||
enables your organization to create, store and share packages in any format, to any place, with total
|
||||
confidence.
|
||||
|
||||
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher debian repository:
|
||||
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-deb)
|
||||
|
||||
```sh
|
||||
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
|
||||
```
|
||||
1. Add Etcher Debian repository:
|
||||
|
||||
2. Trust Bintray.com's GPG key:
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
```sh
|
||||
sudo apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys 379CE192D401AB61
|
||||
```
|
||||
2. Update and install:
|
||||
|
||||
3. Update and install:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install balena-etcher-electron
|
||||
```
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo apt-get remove balena-etcher-electron
|
||||
sudo rm /etc/apt/sources.list.d/balena-etcher.list
|
||||
sudo apt-get update
|
||||
rm /etc/apt/sources.list.d/balena-etcher.list
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
apt-get update
|
||||
```
|
||||
|
||||
##### OpenSUSE LEAP & Tumbleweed install
|
||||
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
|
||||
|
||||
|
||||
##### DNF
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
###### Uninstall
|
||||
|
||||
```sh
|
||||
sudo zypper ar https://balena.io/etcher/static/etcher-rpm.repo
|
||||
sudo zypper ref
|
||||
sudo zypper in balena-etcher-electron
|
||||
rm /etc/yum.repos.d/balena-etcher.repo
|
||||
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||
```
|
||||
|
||||
##### Yum
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
###### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
rm /etc/yum.repos.d/balena-etcher.repo
|
||||
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||
```
|
||||
|
||||
#### OpenSUSE LEAP & Tumbleweed install (zypper)
|
||||
|
||||
1. Add the repo
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
2. Update and install
|
||||
|
||||
```sh
|
||||
sudo zypper up
|
||||
sudo zypper install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo zypper rm balena-etcher-electron
|
||||
```
|
||||
|
||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo yum clean all
|
||||
sudo yum makecache fast
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
# remove the repo
|
||||
sudo zypper rr balena-etcher
|
||||
sudo zypper rr balena-etcher-source
|
||||
```
|
||||
|
||||
#### Solus (GNU/Linux x64)
|
||||
@@ -120,11 +156,10 @@ sudo eopkg it etcher
|
||||
sudo eopkg rm etcher
|
||||
```
|
||||
|
||||
#### Arch Linux / Manjaro (GNU/Linux x64)
|
||||
#### Arch/Manjaro Linux (GNU/Linux x64)
|
||||
|
||||
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
|
||||
|
||||
|
||||
```sh
|
||||
yay -S balena-etcher
|
||||
```
|
||||
@@ -135,22 +170,6 @@ yay -S balena-etcher
|
||||
yay -R balena-etcher
|
||||
```
|
||||
|
||||
#### Brew Cask (macOS)
|
||||
|
||||
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.
|
||||
|
||||
```sh
|
||||
brew cask install balenaetcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
brew cask uninstall balenaetcher
|
||||
```
|
||||
|
||||
#### Chocolatey (Windows)
|
||||
|
||||
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
|
||||
@@ -168,20 +187,22 @@ 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,13 +10,15 @@ async function main(context) {
|
||||
}
|
||||
|
||||
const appName = context.packager.appInfo.productFilename
|
||||
const appleId = 'accounts+apple@balena.io'
|
||||
const appleId = process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io'
|
||||
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD
|
||||
|
||||
// https://github.com/electron/notarize/blob/main/README.md
|
||||
await notarize({
|
||||
appBundleId: 'io.balena.etcher',
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId,
|
||||
appleIdPassword: `@keychain:Application Loader: ${appleId}`
|
||||
appleIdPassword
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
BIN
assets/icon.icns
@@ -1,26 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const cp = require('child_process');
|
||||
const rimraf = require('rimraf');
|
||||
const process = require('process');
|
||||
|
||||
// Rebuild native modules for ia32 and run webpack again for the ia32 part of windows packages
|
||||
exports.default = function(context) {
|
||||
if (context.platform.name === 'windows') {
|
||||
cp.execFileSync(
|
||||
'bash',
|
||||
['./node_modules/.bin/electron-rebuild', '--types', 'dev', '--arch', context.arch],
|
||||
);
|
||||
rimraf.sync('generated');
|
||||
cp.execFileSync(
|
||||
'bash',
|
||||
['./node_modules/.bin/webpack'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_target_arch: context.arch,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
# https://www.electron.build/configuration/configuration
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2020 Balena Ltd
|
||||
copyright: Copyright 2016-2023 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: true
|
||||
publish: null
|
||||
beforeBuild: "./beforeBuild.js"
|
||||
afterPack: "./afterPack.js"
|
||||
afterPack: ./afterPack.js
|
||||
afterSign: ./afterSignHook.js
|
||||
asar: false
|
||||
files:
|
||||
- generated
|
||||
- lib/shared/catalina-sudo/sudo-askpass.osascript.js
|
||||
- lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
|
||||
- lib/shared/catalina-sudo/sudo-askpass.osascript-en.js
|
||||
mac:
|
||||
icon: assets/icon.icns
|
||||
category: public.app-category.developer-tools
|
||||
hardenedRuntime: true
|
||||
entitlements: "entitlements.mac.plist"
|
||||
entitlementsInherit: "entitlements.mac.plist"
|
||||
artifactName: "${productName}-${version}.${ext}"
|
||||
target:
|
||||
- dmg
|
||||
dmg:
|
||||
background: assets/dmg/background.tiff
|
||||
icon: assets/icon.icns
|
||||
@@ -32,6 +34,10 @@ dmg:
|
||||
height: 405
|
||||
win:
|
||||
icon: assets/icon.ico
|
||||
target:
|
||||
- zip
|
||||
- nsis
|
||||
- portable
|
||||
nsis:
|
||||
oneClick: true
|
||||
runAfterFinish: true
|
||||
@@ -44,17 +50,23 @@ portable:
|
||||
artifactName: "${productName}-Portable-${version}.${ext}"
|
||||
requestExecutionLevel: user
|
||||
linux:
|
||||
icon: assets/iconset
|
||||
target:
|
||||
- AppImage
|
||||
- rpm
|
||||
- deb
|
||||
category: Utility
|
||||
packageCategory: utils
|
||||
executableName: balena-etcher-electron
|
||||
executableName: balena-etcher
|
||||
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.
|
||||
icon: assets/iconset
|
||||
appImage:
|
||||
artifactName: ${productName}-${version}-${env.ELECTRON_BUILDER_ARCHITECTURE}.${ext}
|
||||
deb:
|
||||
priority: optional
|
||||
compression: bzip2
|
||||
depends:
|
||||
- gconf2
|
||||
- gconf-service
|
||||
- libappindicator1
|
||||
- gconf2
|
||||
- libasound2
|
||||
- libatk1.0-0
|
||||
- libc6
|
||||
@@ -88,6 +100,7 @@ deb:
|
||||
- libxss1
|
||||
- libxtst6
|
||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||
afterInstall: "./after-install.tpl"
|
||||
rpm:
|
||||
depends:
|
||||
- util-linux
|
||||
|
||||
@@ -23,11 +23,12 @@ import * as ReactDOM from 'react-dom';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as packageJSON from '../../../package.json';
|
||||
import { DrivelistDrive, isSourceDrive } from '../../shared/drive-constraints';
|
||||
import * as EXIT_CODES from '../../shared/exit-codes';
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as availableDrives from './models/available-drives';
|
||||
import * as flashState from './models/flash-state';
|
||||
import { init as ledsInit } from './models/leds';
|
||||
import { deselectImage, getImage } from './models/selection-state';
|
||||
import * as settings from './models/settings';
|
||||
import { Actions, observe, store } from './models/store';
|
||||
import * as analytics from './modules/analytics';
|
||||
@@ -36,15 +37,15 @@ import * as exceptionReporter from './modules/exception-reporter';
|
||||
import * as osDialog from './os/dialog';
|
||||
import * as windowProgress from './os/window-progress';
|
||||
import MainPage from './pages/main/MainPage';
|
||||
import './css/main.css';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
(event: PromiseRejectionEvent | any) => {
|
||||
// Promise: event.reason
|
||||
// Bluebird: event.detail.reason
|
||||
// Anything else: event
|
||||
const error =
|
||||
event.reason || (event.detail && event.detail.reason) || event;
|
||||
const error = event.reason || event;
|
||||
analytics.logException(error);
|
||||
event.preventDefault();
|
||||
},
|
||||
@@ -216,8 +217,7 @@ function prepareDrive(drive: Drive) {
|
||||
disabled: true,
|
||||
icon: 'warning',
|
||||
size: null,
|
||||
link:
|
||||
'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
|
||||
link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
|
||||
linkCTA: 'Install',
|
||||
linkTitle: 'Install missing drivers',
|
||||
linkMessage: outdent`
|
||||
@@ -231,12 +231,12 @@ function prepareDrive(drive: Drive) {
|
||||
}
|
||||
}
|
||||
|
||||
function setDrives(drives: _.Dictionary<any>) {
|
||||
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
|
||||
availableDrives.setDrives(_.values(drives));
|
||||
}
|
||||
|
||||
function getDrives() {
|
||||
return _.keyBy(availableDrives.getDrives() || [], 'device');
|
||||
return _.keyBy(availableDrives.getDrives(), 'device');
|
||||
}
|
||||
|
||||
async function addDrive(drive: Drive) {
|
||||
@@ -250,6 +250,15 @@ async function addDrive(drive: Drive) {
|
||||
}
|
||||
|
||||
function removeDrive(drive: Drive) {
|
||||
if (
|
||||
drive instanceof sdk.sourceDestination.BlockDevice &&
|
||||
// @ts-ignore BlockDevice.drive is private
|
||||
isSourceDrive(drive.drive, getImage())
|
||||
) {
|
||||
// Deselect the image if it was on the drive that was removed.
|
||||
// This will also deselect the image if the drive mountpoints change.
|
||||
deselectImage();
|
||||
}
|
||||
const preparedDrive = prepareDrive(drive);
|
||||
const drives = getDrives();
|
||||
delete drives[preparedDrive.device];
|
||||
@@ -305,9 +314,9 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
|
||||
try {
|
||||
const confirmed = await osDialog.showWarning({
|
||||
confirmationLabel: 'Yes, quit',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: 'Are you sure you want to close Etcher?',
|
||||
confirmationLabel: i18next.t('yesExit'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
title: i18next.t('reallyExit'),
|
||||
description: messages.warning.exitWhileFlashing(),
|
||||
});
|
||||
if (confirmed) {
|
||||
@@ -325,17 +334,31 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
popupExists = false;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
await ledsInit();
|
||||
export async function main() {
|
||||
try {
|
||||
const { init: ledsInit } = require('./models/leds');
|
||||
await ledsInit();
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(MainPage),
|
||||
document.getElementById('main'),
|
||||
// callback to set the correct zoomFactor for webviews as well
|
||||
async () => {
|
||||
const fullscreen = await settings.get('fullscreen');
|
||||
const width = fullscreen ? window.screen.width : window.outerWidth;
|
||||
try {
|
||||
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
563
lib/gui/app/components/drive-selector/drive-selector.tsx
Normal file
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
* 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';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
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: i18next.t('drives.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: i18next.t('drives.size'),
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return prettyBytes(drive.size);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: i18next.t('drives.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 }}
|
||||
>
|
||||
{i18next.t('drives.find', { length: drives.length })}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||
cancel={() => cancel(this.originalList)}
|
||||
done={() => done(selectedList)}
|
||||
action={i18next.t('drives.select', { 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}>
|
||||
{i18next.t('drives.showHidden', {
|
||||
num: numberOfHiddenSystemDrives,
|
||||
})}
|
||||
</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{this.props.showWarnings && hasSystemDrives ? (
|
||||
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||
{i18next.t('drives.systemDriveDanger')}
|
||||
</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={i18next.t('yesContinue')}
|
||||
cancelButtonProps={{
|
||||
children: i18next.t('cancel'),
|
||||
}}
|
||||
children={
|
||||
missingDriversModal.drive.linkMessage ||
|
||||
i18next.t('drives.openInBrowser', {
|
||||
link: missingDriversModal.drive.link,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const DriveStatusWarningModal = ({
|
||||
done,
|
||||
cancel,
|
||||
isSystem,
|
||||
drivesWithWarnings,
|
||||
}: ModalProps & {
|
||||
isSystem: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}) => {
|
||||
let warningSubtitle = i18next.t('drives.largeDriveWarning');
|
||||
let warningCta = i18next.t('drives.largeDriveWarningMsg');
|
||||
|
||||
if (isSystem) {
|
||||
warningSubtitle = i18next.t('drives.systemDriveWarning');
|
||||
warningCta = i18next.t('drives.systemDriveWarningMsg');
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
footerShadow={false}
|
||||
reverseFooterButtons={true}
|
||||
done={done}
|
||||
cancel={cancel}
|
||||
cancelButtonProps={{
|
||||
primary: false,
|
||||
warning: true,
|
||||
children: i18next.t('drives.changeTarget'),
|
||||
}}
|
||||
action={i18next.t('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">
|
||||
{i18next.t('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,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||
|
||||
interface FeaturedProjectProps {
|
||||
onWebviewShow: (isWebviewShowing: boolean) => void;
|
||||
}
|
||||
|
||||
interface FeaturedProjectState {
|
||||
endpoint: string | null;
|
||||
}
|
||||
|
||||
export class FeaturedProject extends React.Component<
|
||||
FeaturedProjectProps,
|
||||
FeaturedProjectState
|
||||
> {
|
||||
constructor(props: FeaturedProjectProps) {
|
||||
super(props);
|
||||
this.state = { endpoint: null };
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
try {
|
||||
const endpoint =
|
||||
(await settings.get('featuredProjectEndpoint')) ||
|
||||
'https://assets.balena.io/etcher-featured/index.html';
|
||||
this.setState({ endpoint });
|
||||
} catch (error) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.state.endpoint ? (
|
||||
<SafeWebview src={this.state.endpoint} {...this.props}></SafeWebview>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
@@ -14,21 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'rendition';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
import { Actions, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { FlashAnother } from '../flash-another/flash-another';
|
||||
import { FlashResults } from '../flash-results/flash-results';
|
||||
|
||||
import EtcherSvg from '../../../assets/etcher.svg';
|
||||
import LoveSvg from '../../../assets/love.svg';
|
||||
import BalenaSvg from '../../../assets/balena.svg';
|
||||
import { FlashResults, FlashError } from '../flash-results/flash-results';
|
||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||
|
||||
function restart(goToMain: () => void) {
|
||||
selectionState.deselectAllDrives();
|
||||
@@ -43,63 +40,84 @@ function restart(goToMain: () => void) {
|
||||
goToMain();
|
||||
}
|
||||
|
||||
function formattedErrors() {
|
||||
const errors = _.map(
|
||||
_.get(flashState.getFlashResults(), ['results', 'errors']),
|
||||
(error) => {
|
||||
return `${error.device}: ${error.message || error.code}`;
|
||||
},
|
||||
async function getSuccessBannerURL() {
|
||||
return (
|
||||
(await settings.get('successBannerURL')) ??
|
||||
'https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true'
|
||||
);
|
||||
return errors.join('\n');
|
||||
}
|
||||
|
||||
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
const results = flashState.getFlashResults().results || {};
|
||||
const [webviewShowing, setWebviewShowing] = React.useState(false);
|
||||
const [successBannerURL, setSuccessBannerURL] = React.useState('');
|
||||
(async () => {
|
||||
setSuccessBannerURL(await getSuccessBannerURL());
|
||||
})();
|
||||
const flashResults = flashState.getFlashResults();
|
||||
const errors: FlashError[] = (
|
||||
store.getState().toJS().failedDeviceErrors || []
|
||||
).map(([, error]: [string, FlashError]) => ({
|
||||
...error,
|
||||
}));
|
||||
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
|
||||
flashState.getFlashState();
|
||||
const {
|
||||
skip,
|
||||
results = {
|
||||
bytesWritten,
|
||||
sourceMetadata: {
|
||||
size,
|
||||
blockmappedSize,
|
||||
},
|
||||
averageFlashingSpeed: averageSpeed,
|
||||
devices: { failed, successful: 0 },
|
||||
},
|
||||
} = flashResults;
|
||||
return (
|
||||
<div className="page-finish row around-xs">
|
||||
<div className="col-xs">
|
||||
<div className="box center">
|
||||
<FlashResults results={results} errors={formattedErrors()} />
|
||||
<Flex height="100%" justifyContent="space-between">
|
||||
<Flex
|
||||
width={webviewShowing ? '36.2vw' : '100vw'}
|
||||
height="100vh"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<FlashResults
|
||||
image={selectionState.getImage()?.name}
|
||||
results={results}
|
||||
skip={skip}
|
||||
errors={errors}
|
||||
mb="32px"
|
||||
goToMain={goToMain}
|
||||
/>
|
||||
|
||||
<FlashAnother
|
||||
onClick={() => {
|
||||
restart(goToMain);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="box center">
|
||||
<div className="fallback-banner">
|
||||
<div className="caption-big">
|
||||
Thanks for using
|
||||
<span
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://balena.io/etcher?ref=etcher_offline_banner',
|
||||
)
|
||||
}
|
||||
>
|
||||
<EtcherSvg width="165px" style={{ margin: '0 10px' }} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="caption-small fallback-footer">
|
||||
made with
|
||||
<LoveSvg height="20px" style={{ margin: '0 10px' }} />
|
||||
by
|
||||
<span
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
openExternal('https://balena.io?ref=etcher_success')
|
||||
}
|
||||
>
|
||||
<BalenaSvg height="20px" style={{ margin: '0 10px' }} />
|
||||
</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,15 +15,9 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { BaseButton } from '../../styled-components';
|
||||
|
||||
const FlashAnotherButton = styled(BaseButton)`
|
||||
position: absolute;
|
||||
right: 152px;
|
||||
top: 60px;
|
||||
`;
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export interface FlashAnotherProps {
|
||||
onClick: () => void;
|
||||
@@ -31,8 +25,8 @@ export interface FlashAnotherProps {
|
||||
|
||||
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||
return (
|
||||
<FlashAnotherButton primary onClick={props.onClick}>
|
||||
Flash Another
|
||||
</FlashAnotherButton>
|
||||
<BaseButton primary onClick={props.onClick}>
|
||||
{i18next.t('flash.another')}
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,102 +14,230 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as _ from 'lodash';
|
||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
|
||||
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import { Txt, Flex } from 'rendition';
|
||||
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
import { left, position, space, top } from 'styled-system';
|
||||
|
||||
import { progress } from '../../../../shared/messages';
|
||||
import { bytesToMegabytes } from '../../../../shared/units';
|
||||
import { Underline } from '../../styled-components';
|
||||
|
||||
const Div = styled.div<any>`
|
||||
${position}
|
||||
${top}
|
||||
${left}
|
||||
${space}
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
import { getDrives } from '../../models/available-drives';
|
||||
import { resetState } from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { Modal, Table } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
|
||||
&&& [data-display='table-head'],
|
||||
&&& [data-display='table-body'] {
|
||||
> [data-display='table-row'] {
|
||||
> [data-display='table-cell'] {
|
||||
&:first-child {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const DoneIcon = (props: {
|
||||
skipped: boolean;
|
||||
color: string;
|
||||
allFailed: boolean;
|
||||
}) => {
|
||||
const svgProps = {
|
||||
width: '28px',
|
||||
fill: props.color,
|
||||
style: {
|
||||
marginTop: '-25px',
|
||||
marginLeft: '13px',
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
return props.allFailed && !props.skipped ? (
|
||||
<TimesCircleSvg {...svgProps} />
|
||||
) : (
|
||||
<CheckCircleSvg {...svgProps} />
|
||||
);
|
||||
};
|
||||
|
||||
export interface FlashError extends Error {
|
||||
description: string;
|
||||
device: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
function formattedErrors(errors: FlashError[]) {
|
||||
return errors
|
||||
.map((error) => `${error.device}: ${error.message || error.code}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<FlashError>> = [
|
||||
{
|
||||
field: 'description',
|
||||
label: i18next.t('flash.target'),
|
||||
},
|
||||
{
|
||||
field: 'device',
|
||||
label: i18next.t('flash.location'),
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
label: i18next.t('flash.error'),
|
||||
render: (message: string, { code }: FlashError) => {
|
||||
return message ?? code;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function getEffectiveSpeed(results: {
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
}) {
|
||||
const flashedSize =
|
||||
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
|
||||
const timeSpent = flashedSize / results.averageFlashingSpeed;
|
||||
return results.sourceMetadata.size / timeSpent;
|
||||
}
|
||||
|
||||
export function FlashResults({
|
||||
goToMain,
|
||||
image = '',
|
||||
errors,
|
||||
results,
|
||||
skip,
|
||||
...props
|
||||
}: {
|
||||
errors: string;
|
||||
goToMain: () => void;
|
||||
image?: string;
|
||||
errors: FlashError[];
|
||||
skip: boolean;
|
||||
results: {
|
||||
bytesWritten: number;
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
devices: { failed: number; successful: number };
|
||||
};
|
||||
}) {
|
||||
const allDevicesFailed = results.devices.successful === 0;
|
||||
const effectiveSpeed = _.round(
|
||||
bytesToMegabytes(
|
||||
results.sourceMetadata.size /
|
||||
(results.bytesWritten / results.averageFlashingSpeed),
|
||||
),
|
||||
} & FlexProps) {
|
||||
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||
const allFailed = !skip && results.devices.successful === 0;
|
||||
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
|
||||
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
|
||||
1,
|
||||
);
|
||||
return (
|
||||
<Div position="absolute" left="153px" top="66px">
|
||||
<Flex alignItems="center">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
color={allDevicesFailed ? '#c6c8c9' : '#1ac135'}
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
margin: '0 15px 0 0',
|
||||
}}
|
||||
/>
|
||||
<Txt fontSize={24} color="#fff">
|
||||
Flash Complete!
|
||||
<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">
|
||||
{allFailed
|
||||
? i18next.t('flash.flashFailed')
|
||||
: i18next.t('flash.flashCompleted')}
|
||||
</Txt>
|
||||
{skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
|
||||
</Flex>
|
||||
<Div className="results" mr="0" mb="0" ml="40px">
|
||||
{_.map(results.devices, (quantity, type) => {
|
||||
return quantity ? (
|
||||
<Underline
|
||||
tooltip={type === 'failed' ? errors : undefined}
|
||||
key={type}
|
||||
>
|
||||
<div
|
||||
key={type}
|
||||
className={`target-status-line target-status-${type}`}
|
||||
>
|
||||
<span className="target-status-dot"></span>
|
||||
<span className="target-status-quantity">{quantity}</span>
|
||||
<span className="target-status-message">
|
||||
{progress[type](quantity)}
|
||||
</span>
|
||||
</div>
|
||||
</Underline>
|
||||
) : null;
|
||||
})}
|
||||
{!allDevicesFailed && (
|
||||
<Flex flexDirection="column" 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)}>
|
||||
{i18next.t('flash.moreInfo')}
|
||||
</Link>
|
||||
</Flex>
|
||||
) : null}
|
||||
{!allFailed && (
|
||||
<Txt
|
||||
color="#787c7f"
|
||||
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.
|
||||
`}
|
||||
tooltip={i18next.t('flash.speedTip')}
|
||||
>
|
||||
Effective speed: {effectiveSpeed} MB/s
|
||||
{i18next.t('flash.speed', { speed: effectiveSpeed })}
|
||||
</Txt>
|
||||
)}
|
||||
</Div>
|
||||
</Div>
|
||||
</Flex>
|
||||
|
||||
{showErrorsInfo && (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
{i18next.t('failedTarget')}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
action={i18next.t('failedRetry')}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,25 +15,27 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button, Flex, ProgressBar, Txt } from 'rendition';
|
||||
import { Flex, Button, ProgressBar, Txt } from 'rendition';
|
||||
import { default as styled } from 'styled-components';
|
||||
|
||||
import { fromFlashState } from '../../modules/progress-status';
|
||||
import { StepButton } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
> div {
|
||||
width: 220px;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
transition-duration: 0s;
|
||||
|
||||
> div {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
width: 220px;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 14px;
|
||||
@@ -49,7 +51,7 @@ interface ProgressButtonProps {
|
||||
percentage: number;
|
||||
position: number;
|
||||
disabled: boolean;
|
||||
cancel: () => void;
|
||||
cancel: (type: string) => void;
|
||||
callback: () => void;
|
||||
warning?: boolean;
|
||||
}
|
||||
@@ -60,12 +62,16 @@ const colors = {
|
||||
verifying: '#1ac135',
|
||||
} as const;
|
||||
|
||||
const CancelButton = styled((props) => (
|
||||
<Button plain {...props}>
|
||||
Cancel
|
||||
</Button>
|
||||
))`
|
||||
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
|
||||
return (
|
||||
<Button plain onClick={() => onClick(status)} {...props}>
|
||||
{status}
|
||||
</Button>
|
||||
);
|
||||
})`
|
||||
font-weight: 600;
|
||||
|
||||
&&& {
|
||||
width: auto;
|
||||
height: auto;
|
||||
@@ -75,16 +81,21 @@ const CancelButton = styled((props) => (
|
||||
|
||||
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
public render() {
|
||||
const percentage = this.props.percentage;
|
||||
const warning = this.props.warning;
|
||||
const { status, position } = fromFlashState({
|
||||
type: this.props.type,
|
||||
percentage,
|
||||
position: this.props.position,
|
||||
percentage: this.props.percentage,
|
||||
});
|
||||
const type = this.props.type || 'default';
|
||||
if (this.props.active) {
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
alignItems="baseline"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
style={{
|
||||
marginTop: 42,
|
||||
marginBottom: '6px',
|
||||
@@ -94,25 +105,31 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
>
|
||||
<Flex>
|
||||
<Txt color="#fff">{status} </Txt>
|
||||
<Txt color={colors[this.props.type]}>{position}</Txt>
|
||||
<Txt color={colors[type]}>{position}</Txt>
|
||||
</Flex>
|
||||
<CancelButton onClick={this.props.cancel} color="#00aeef" />
|
||||
{type && (
|
||||
<CancelButton
|
||||
type={type}
|
||||
onClick={this.props.cancel}
|
||||
color="#00aeef"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<FlashProgressBar
|
||||
background={colors[this.props.type]}
|
||||
value={this.props.percentage}
|
||||
/>
|
||||
<FlashProgressBar background={colors[type]} value={percentage} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StepButton
|
||||
primary={!this.props.warning}
|
||||
warning={this.props.warning}
|
||||
primary={!warning}
|
||||
warning={warning}
|
||||
onClick={this.props.callback}
|
||||
disabled={this.props.disabled}
|
||||
style={{
|
||||
marginTop: 30,
|
||||
}}
|
||||
>
|
||||
Flash!
|
||||
{i18next.t('flash.flashNow')}
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,80 +15,60 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { default as styled } from 'styled-components';
|
||||
import { color } from 'styled-system';
|
||||
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`;
|
||||
|
||||
const Span = styled.span`
|
||||
${color}
|
||||
`;
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
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
|
||||
width="20px"
|
||||
width="21px"
|
||||
height="21px"
|
||||
contents={this.props.imageLogo}
|
||||
fallback={<ImageSvg className="disabled" width="20px" />}
|
||||
fallback={ImageSvg}
|
||||
style={{ marginRight: '9px' }}
|
||||
/>
|
||||
<Span>{this.props.imageName}</Span>
|
||||
<Span color="#7e8085">{this.props.imageSize}</Span>
|
||||
</Span>
|
||||
<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">
|
||||
<DriveSvg className="disabled" width="20px" />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,9 @@ const API_VERSION = '2';
|
||||
interface SafeWebviewProps {
|
||||
// The website source URL
|
||||
src: string;
|
||||
// @summary Refresh the webview
|
||||
refreshNow?: boolean;
|
||||
// Webview lifecycle event
|
||||
onWebviewShow?: (isWebviewShowing: boolean) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
interface SafeWebviewState {
|
||||
@@ -109,15 +108,18 @@ export class SafeWebview extends React.PureComponent<
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
style = {
|
||||
flex: this.state.shouldShow ? undefined : '0 1',
|
||||
width: this.state.shouldShow ? undefined : '0',
|
||||
height: this.state.shouldShow ? undefined : '0',
|
||||
},
|
||||
} = this.props;
|
||||
return (
|
||||
<webview
|
||||
ref={this.webviewRef}
|
||||
partition={ELECTRON_SESSION}
|
||||
style={{
|
||||
flex: this.state.shouldShow ? undefined : '0 1',
|
||||
width: this.state.shouldShow ? undefined : '0',
|
||||
height: this.state.shouldShow ? undefined : '0',
|
||||
}}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,98 +14,57 @@
|
||||
* 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 { Checkbox, Modal } from 'rendition';
|
||||
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
|
||||
|
||||
import { version } from '../../../../../package.json';
|
||||
import { version, packageType } from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
|
||||
const platform = os.platform();
|
||||
|
||||
interface WarningModalProps {
|
||||
message: string;
|
||||
confirmLabel: string;
|
||||
cancel: () => void;
|
||||
done: () => void;
|
||||
}
|
||||
|
||||
const WarningModal = ({
|
||||
message,
|
||||
confirmLabel,
|
||||
cancel,
|
||||
done,
|
||||
}: WarningModalProps) => {
|
||||
return (
|
||||
<Modal
|
||||
title={confirmLabel}
|
||||
action={confirmLabel}
|
||||
cancel={cancel}
|
||||
done={done}
|
||||
style={{
|
||||
width: 420,
|
||||
height: 300,
|
||||
}}
|
||||
primaryButtonProps={{ warning: true }}
|
||||
>
|
||||
{message}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
import { Modal } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
import { etcherProInfo } from '../../utils/etcher-pro-specific';
|
||||
|
||||
interface Setting {
|
||||
name: string;
|
||||
label: string | JSX.Element;
|
||||
options?: {
|
||||
description: string;
|
||||
confirmLabel: string;
|
||||
};
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
async function getSettingsList(): Promise<Setting[]> {
|
||||
return [
|
||||
const list: Setting[] = [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||
label: i18next.t('settings.errorReporting'),
|
||||
},
|
||||
{
|
||||
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: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
name: 'autoBlockmapping',
|
||||
label: i18next.t('settings.trimExtPartitions'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface Warning {
|
||||
setting: string;
|
||||
settingValue: boolean;
|
||||
description: string;
|
||||
confirmLabel: string;
|
||||
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||
list.push({
|
||||
name: 'updatesEnabled',
|
||||
label: i18next.t('settings.autoUpdate'),
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const EPInfo = etcherProInfo();
|
||||
|
||||
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(() => {
|
||||
@@ -125,93 +84,73 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
}
|
||||
})();
|
||||
});
|
||||
const [warning, setWarning] = React.useState<Warning | undefined>(undefined);
|
||||
|
||||
const toggleSetting = async (
|
||||
setting: string,
|
||||
options?: Setting['options'],
|
||||
) => {
|
||||
const toggleSetting = async (setting: string) => {
|
||||
const value = currentSettings[setting];
|
||||
const dangerous = options !== undefined;
|
||||
|
||||
analytics.logEvent('Toggle setting', {
|
||||
setting,
|
||||
value,
|
||||
dangerous,
|
||||
analytics.logEvent('Toggle setting', { setting, value });
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
|
||||
if (value || options === undefined) {
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
setWarning(undefined);
|
||||
return;
|
||||
} else {
|
||||
// Show warning since it's a dangerous setting
|
||||
setWarning({
|
||||
setting,
|
||||
settingValue: value,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
id="settings-modal"
|
||||
title="Settings"
|
||||
titleElement={
|
||||
<Txt fontSize={24} mb={24}>
|
||||
{i18next.t('settings.settings')}
|
||||
</Txt>
|
||||
}
|
||||
done={() => toggleModal(false)}
|
||||
style={{
|
||||
width: 780,
|
||||
height: 420,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{_.map(settingsList, (setting: Setting, i: number) => {
|
||||
return setting.hide ? null : (
|
||||
<div key={setting.name}>
|
||||
<Flex flexDirection="column">
|
||||
{settingsList.map((setting: Setting, i: number) => {
|
||||
return (
|
||||
<Flex key={setting.name} mb={14}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
label={setting.label}
|
||||
checked={currentSettings[setting.name]}
|
||||
onChange={() => toggleSetting(setting.name, setting.options)}
|
||||
onChange={() => toggleSetting(setting.name)}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
<div>
|
||||
<span
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} /> {version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{warning === undefined ? null : (
|
||||
<WarningModal
|
||||
message={warning.description}
|
||||
confirmLabel={warning.confirmLabel}
|
||||
done={async () => {
|
||||
await settings.set(warning.setting, !warning.settingValue);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[warning.setting]: true,
|
||||
});
|
||||
setWarning(undefined);
|
||||
{EPInfo !== undefined && (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={24}>{i18next.t('settings.systemInformation')}</Txt>
|
||||
{EPInfo.get_serial() === undefined ? (
|
||||
<InfoBox label="UUID" value={EPInfo.uuid} />
|
||||
) : (
|
||||
<InfoBox label="Serial" value={EPInfo.get_serial()} />
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
mt={18}
|
||||
alignItems="center"
|
||||
color="#00aeef"
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
}}
|
||||
cancel={() => {
|
||||
setWarning(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
|
||||
)
|
||||
}
|
||||
>
|
||||
<GithubSvg
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,15 +37,17 @@ function tryParseSVGContents(contents?: string): string | undefined {
|
||||
}
|
||||
|
||||
interface SVGIconProps {
|
||||
// List of embedded SVG contents to be tried in succession if any fails
|
||||
contents: string;
|
||||
fallback: JSX.Element;
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,17 +56,19 @@ interface SVGIconProps {
|
||||
export class SVGIcon extends React.PureComponent<SVGIconProps> {
|
||||
public render() {
|
||||
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) {
|
||||
const width = this.props.width || DEFAULT_SIZE;
|
||||
const height = this.props.height || DEFAULT_SIZE;
|
||||
return (
|
||||
<img
|
||||
className={this.props.disabled ? 'disabled' : ''}
|
||||
style={{ width, height }}
|
||||
style={style}
|
||||
src={svgData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return this.props.fallback;
|
||||
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,
|
||||
@@ -33,10 +32,7 @@ import {
|
||||
StepNameButton,
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
const TargetDetail = styled((props) => <Txt.span {...props}></Txt.span>)`
|
||||
float: ${({ float }) => float};
|
||||
`;
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
interface TargetSelectorProps {
|
||||
targets: any[];
|
||||
@@ -46,49 +42,66 @@ interface TargetSelectorProps {
|
||||
flashing: boolean;
|
||||
show: boolean;
|
||||
tooltip: string;
|
||||
image: Image;
|
||||
}
|
||||
|
||||
function DriveCompatibilityWarning(props: {
|
||||
drive: DrivelistDrive;
|
||||
image: Image;
|
||||
}) {
|
||||
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
|
||||
props.drive,
|
||||
props.image,
|
||||
);
|
||||
if (compatibilityWarnings.length === 0) {
|
||||
return null;
|
||||
function getDriveWarning(status: DriveStatus) {
|
||||
switch (status.message) {
|
||||
case compatibility.containsImage():
|
||||
return warning.sourceDrive();
|
||||
case compatibility.largeDrive():
|
||||
return warning.largeDriveSize();
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
const messages = _.map(compatibilityWarnings, 'message');
|
||||
return (
|
||||
<Txt.span
|
||||
className="glyphicon glyphicon-exclamation-sign"
|
||||
ml={2}
|
||||
tooltip={messages.join(', ')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TargetSelector(props: TargetSelectorProps) {
|
||||
const DriveCompatibilityWarning = ({
|
||||
warnings,
|
||||
...props
|
||||
}: {
|
||||
warnings: string[];
|
||||
} & FlexProps) => {
|
||||
const systemDrive = warnings.find(
|
||||
(message) => message === warning.systemDrive(),
|
||||
);
|
||||
return (
|
||||
<Flex tooltip={warnings.join(', ')} {...props}>
|
||||
<ExclamationTriangleSvg
|
||||
fill={systemDrive ? '#fca321' : '#8f9297'}
|
||||
height="1em"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
const targets = getSelectedDrives();
|
||||
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0];
|
||||
const warnings = getDriveImageCompatibilityStatuses(
|
||||
target,
|
||||
getImage(),
|
||||
true,
|
||||
).map(getDriveWarning);
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{warnings.length > 0 && (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
)}
|
||||
{middleEllipsis(target.description, 20)}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
|
||||
Change
|
||||
{i18next.t('target.change')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
<DetailsText>
|
||||
<DriveCompatibilityWarning drive={target} image={props.image} />
|
||||
{bytesToClosestUnit(target.size)}
|
||||
</DetailsText>
|
||||
{target.size != null && (
|
||||
<DetailsText>{prettyBytes(target.size)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -96,34 +109,35 @@ export function TargetSelector(props: TargetSelectorProps) {
|
||||
if (targets.length > 1) {
|
||||
const targetsTemplate = [];
|
||||
for (const target of targets) {
|
||||
const warnings = getDriveImageCompatibilityStatuses(
|
||||
target,
|
||||
getImage(),
|
||||
true,
|
||||
).map(getDriveWarning);
|
||||
targetsTemplate.push(
|
||||
<DetailsText
|
||||
key={target.device}
|
||||
tooltip={`${target.description} ${
|
||||
target.displayName
|
||||
} ${bytesToClosestUnit(target.size)}`}
|
||||
tooltip={`${target.description} ${target.displayName} ${
|
||||
target.size != null ? prettyBytes(target.size) : ''
|
||||
}`}
|
||||
px={21}
|
||||
>
|
||||
<Txt.span>
|
||||
<DriveCompatibilityWarning drive={target} image={props.image} />
|
||||
<TargetDetail float="left">
|
||||
{middleEllipsis(target.description, 14)}
|
||||
</TargetDetail>
|
||||
<TargetDetail float="right">
|
||||
{bytesToClosestUnit(target.size)}
|
||||
</TargetDetail>
|
||||
</Txt.span>
|
||||
{warnings.length > 0 ? (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
) : null}
|
||||
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
|
||||
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
|
||||
</DetailsText>,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip} fontSize={16}>
|
||||
{targets.length} Targets
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{targets.length} {i18next.t('target.targets')}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
|
||||
Change
|
||||
{i18next.t('target.change')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{targetsTemplate}
|
||||
@@ -138,7 +152,7 @@ export function TargetSelector(props: TargetSelectorProps) {
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
Select target
|
||||
{i18next.t('target.selectTarget')}
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,489 +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 {
|
||||
faChevronDown,
|
||||
faExclamationTriangle,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { scanner, sourceDestination } from 'etcher-sdk';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Table,
|
||||
Txt,
|
||||
Flex,
|
||||
Link,
|
||||
TableColumn,
|
||||
ModalProps,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
hasListDriveImageCompatibilityStatus,
|
||||
isDriveValid,
|
||||
TargetStatus,
|
||||
Image,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility } from '../../../../shared/messages';
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import {
|
||||
getImage,
|
||||
getSelectedDrives,
|
||||
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 { Modal } from '../../styled-components';
|
||||
|
||||
import TargetSVGIcon from '../../../assets/tgt.svg';
|
||||
|
||||
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 Target = scanner.adapters.DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||
|
||||
function isUsbbootDrive(drive: Target): drive is UsbbootDrive {
|
||||
return (drive as UsbbootDrive).progress !== undefined;
|
||||
}
|
||||
|
||||
function isDriverlessDrive(drive: Target): drive is DriverlessDrive {
|
||||
return (drive as DriverlessDrive).link !== undefined;
|
||||
}
|
||||
|
||||
function isDrivelistDrive(
|
||||
drive: Target,
|
||||
): drive is scanner.adapters.DrivelistDrive {
|
||||
return typeof (drive as scanner.adapters.DrivelistDrive).size === 'number';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
const TargetsTable = styled(({ refFn, ...props }) => {
|
||||
return (
|
||||
<div>
|
||||
<Table<Target> ref={refFn} {...props} />
|
||||
</div>
|
||||
);
|
||||
})`
|
||||
[data-display='table-head'] [data-display='table-cell'] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: ${(props) => props.theme.colors.quartenary.light};
|
||||
}
|
||||
|
||||
[data-display='table-cell']:first-child {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
[data-display='table-cell']:last-child {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
&& [data-display='table-row'] > [data-display='table-cell'] {
|
||||
padding: 6px 8px;
|
||||
color: #2a506f;
|
||||
}
|
||||
`;
|
||||
|
||||
function badgeShadeFromStatus(status: string) {
|
||||
switch (status) {
|
||||
case compatibility.containsImage():
|
||||
return 16;
|
||||
case compatibility.system():
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
interface TargetSelectorModalProps extends Omit<ModalProps, 'done'> {
|
||||
done: (targets: scanner.adapters.DrivelistDrive[]) => void;
|
||||
}
|
||||
|
||||
interface TargetSelectorModalState {
|
||||
drives: Target[];
|
||||
image: Image;
|
||||
missingDriversModal: { drive?: DriverlessDrive };
|
||||
selectedList: scanner.adapters.DrivelistDrive[];
|
||||
showSystemDrives: boolean;
|
||||
}
|
||||
|
||||
export class TargetSelectorModal extends React.Component<
|
||||
TargetSelectorModalProps,
|
||||
TargetSelectorModalState
|
||||
> {
|
||||
unsubscribe: () => void;
|
||||
tableColumns: Array<TableColumn<Target>>;
|
||||
|
||||
constructor(props: TargetSelectorModalProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = getSelectedDrives();
|
||||
|
||||
this.state = {
|
||||
drives: getDrives(),
|
||||
image: getImage(),
|
||||
missingDriversModal: defaultMissingDriversModalState,
|
||||
selectedList,
|
||||
showSystemDrives: false,
|
||||
};
|
||||
|
||||
this.tableColumns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
render: (description: string, drive: Target) => {
|
||||
return isDrivelistDrive(drive) && drive.isSystem ? (
|
||||
<Flex alignItems="center">
|
||||
<FontAwesomeIcon
|
||||
style={{ color: '#fca321' }}
|
||||
icon={faExclamationTriangle}
|
||||
/>
|
||||
<Txt ml={8}>{description}</Txt>
|
||||
</Flex>
|
||||
) : (
|
||||
<Txt>{description}</Txt>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'size',
|
||||
label: 'Size',
|
||||
render: (_description: string, drive: Target) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return bytesToClosestUnit(drive.size);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: 'Location',
|
||||
render: (_description: string, drive: Target) => {
|
||||
return (
|
||||
<Txt>
|
||||
{drive.displayName}
|
||||
{isDriverlessDrive(drive) && (
|
||||
<>
|
||||
{' '}
|
||||
-{' '}
|
||||
<b>
|
||||
<a onClick={() => this.installMissingDrivers(drive)}>
|
||||
{drive.linkCTA}
|
||||
</a>
|
||||
</b>
|
||||
</>
|
||||
)}
|
||||
</Txt>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'extra',
|
||||
// Space as empty string would use the field name as label
|
||||
label: ' ',
|
||||
render: (_description: string, drive: Target) => {
|
||||
if (isUsbbootDrive(drive)) {
|
||||
return this.renderProgress(drive.progress);
|
||||
} else if (isDrivelistDrive(drive)) {
|
||||
return this.renderStatuses(
|
||||
getDriveImageCompatibilityStatuses(drive, this.state.image),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private driveShouldBeDisabled(drive: Target, image: any) {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
!isDriveValid(drive, image)
|
||||
);
|
||||
}
|
||||
|
||||
private getDisplayedTargets(targets: Target[]): Target[] {
|
||||
return targets.filter((drive) => {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
isDriveSelected(drive.device) ||
|
||||
this.state.showSystemDrives ||
|
||||
!drive.isSystem
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private getDisabledTargets(drives: Target[], image: any): 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 renderStatuses(statuses: TargetStatus[]) {
|
||||
return (
|
||||
// the column render fn expects a single Element
|
||||
<>
|
||||
{statuses.map((status) => {
|
||||
const badgeShade = badgeShadeFromStatus(status.message);
|
||||
return (
|
||||
<Badge key={status.message} shade={badgeShade}>
|
||||
{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: getSelectedDrives(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { cancel, done, ...props } = this.props;
|
||||
const { selectedList, drives, image, missingDriversModal } = this.state;
|
||||
|
||||
const displayedTargets = this.getDisplayedTargets(drives);
|
||||
const disabledTargets = this.getDisabledTargets(drives, image);
|
||||
const numberOfSystemDrives = drives.filter(
|
||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
||||
).length;
|
||||
const numberOfDisplayedSystemDrives = displayedTargets.filter(
|
||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
||||
).length;
|
||||
const numberOfHiddenSystemDrives =
|
||||
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||
const hasStatus = hasListDriveImageCompatibilityStatus(selectedList, image);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
Select target
|
||||
</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}
|
||||
done={() => done(selectedList)}
|
||||
action={`Select (${selectedList.length})`}
|
||||
style={{
|
||||
width: '780px',
|
||||
height: '420px',
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
primary: !hasStatus,
|
||||
warning: hasStatus,
|
||||
disabled: !hasAvailableDrives(),
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Flex width="100%" height="100%">
|
||||
{!hasAvailableDrives() ? (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
<TargetSVGIcon width="40px" height="90px" />
|
||||
<b>Plug a target drive</b>
|
||||
</Flex>
|
||||
) : (
|
||||
<ScrollableFlex
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="calc(100% - 15px)"
|
||||
>
|
||||
<TargetsTable
|
||||
refFn={(t: Table<Target>) => {
|
||||
if (t !== null) {
|
||||
t.setRowSelection(selectedList);
|
||||
}
|
||||
}}
|
||||
columns={this.tableColumns}
|
||||
data={displayedTargets}
|
||||
disabledRows={disabledTargets}
|
||||
rowKey="displayName"
|
||||
onCheck={(rows: Target[]) => {
|
||||
this.setState({
|
||||
selectedList: rows.filter(isDrivelistDrive),
|
||||
});
|
||||
}}
|
||||
onRowClick={(row: Target) => {
|
||||
if (
|
||||
!isDrivelistDrive(row) ||
|
||||
this.driveShouldBeDisabled(row, image)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const newList = [...selectedList];
|
||||
const selectedIndex = selectedList.findIndex(
|
||||
(target) => target.device === row.device,
|
||||
);
|
||||
if (selectedIndex === -1) {
|
||||
newList.push(row);
|
||||
} else {
|
||||
// Deselect if selected
|
||||
newList.splice(selectedIndex, 1);
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newList,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{numberOfHiddenSystemDrives > 0 && (
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
onClick={() => this.setState({ showSystemDrives: true })}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</ScrollableFlex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{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) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
194
lib/gui/app/components/target-selector/target-selector.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* 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';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
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={i18next.t('target.selectTarget')}
|
||||
emptyListLabel={i18next.t('target.plugTarget')}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
42
lib/gui/app/i18n.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import zh_CN_translation from './i18n/zh-CN';
|
||||
import zh_TW_translation from './i18n/zh-TW';
|
||||
import en_translation from './i18n/en';
|
||||
|
||||
export function langParser() {
|
||||
if (process.env.LANG !== undefined) {
|
||||
// Bypass mocha, where lang-detect don't works
|
||||
return 'en';
|
||||
}
|
||||
|
||||
const lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
|
||||
switch (lang.substr(0, 2)) {
|
||||
case 'zh':
|
||||
if (lang === 'zh-CN' || lang === 'zh-SG') {
|
||||
return 'zh-CN';
|
||||
} // Simplified Chinese
|
||||
else {
|
||||
return 'zh-TW';
|
||||
} // Traditional Chinese
|
||||
default:
|
||||
return lang.substr(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
i18next.use(initReactI18next).init({
|
||||
lng: langParser(),
|
||||
fallbackLng: 'en',
|
||||
nonExplicitSupportedLngs: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
resources: {
|
||||
'zh-CN': zh_CN_translation,
|
||||
'zh-TW': zh_TW_translation,
|
||||
en: en_translation,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18next;
|
||||
23
lib/gui/app/i18n/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# i18n
|
||||
|
||||
## How it was done
|
||||
|
||||
Using the open-source lib [i18next](https://www.i18next.com/).
|
||||
|
||||
## How to add your own language
|
||||
|
||||
1. Go to `lib/gui/app/i18n` and add a file named `xx.ts` (use the codes mentioned
|
||||
in [the link](https://www.science.co.il/language/Locale-codes.php), and we support styles as `fr`, `de`, `es-ES`
|
||||
and `pt-BR`)
|
||||
.
|
||||
2. Copy the content from an existing translation and start to translate.
|
||||
3. Once done, go to `lib/gui/app/i18n.ts` and add a line of `import xx_translation from './i18n/xx'` after the
|
||||
already-added imports and add `xx: xx_translation` in the `resources` section of `i18next.init()` function.
|
||||
4. Now go to `lib/shared/catalina-sudo/` and copy the `sudo-askpass.osascript-en.js`, change it to
|
||||
be `sudo-askpass.osascript-xx.js` and edit
|
||||
the `'balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.'` line and
|
||||
those `Ok`s and `Cancel`s to your own language.
|
||||
5. If, your language has several variations when they are used in several countries/regions, such as `zh-CN` and `zh-TW`
|
||||
, or `pt-BR` and `pt-PT`, edit
|
||||
the `langParser()` in the `lib/gui/app/i18n.ts` file to meet your need.
|
||||
6. Make a commit, and then a pull request on GitHub.
|
||||
161
lib/gui/app/i18n/en.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
continue: 'Continue',
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
skip: 'Skip',
|
||||
sure: "Yes, I'm sure",
|
||||
warning: 'WARNING! ',
|
||||
attention: 'Attention',
|
||||
failed: 'Failed',
|
||||
completed: 'Completed',
|
||||
yesContinue: 'Yes, continue',
|
||||
reallyExit: 'Are you sure you want to close Etcher?',
|
||||
yesExit: 'Yes, quit',
|
||||
progress: {
|
||||
starting: 'Starting...',
|
||||
decompressing: 'Decompressing...',
|
||||
flashing: 'Flashing...',
|
||||
finishing: 'Finishing...',
|
||||
verifying: 'Validating...',
|
||||
failing: 'Failed',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: 'Not recommended',
|
||||
tooSmall: 'Too small',
|
||||
locked: 'Locked',
|
||||
system: 'System drive',
|
||||
containsImage: 'Source drive',
|
||||
largeDrive: 'Large drive',
|
||||
sourceLarger: 'The selected source is {{byte}} larger than this drive.',
|
||||
flashSucceed_one: 'Successful target',
|
||||
flashSucceed_other: 'Successful targets',
|
||||
flashFail_one: 'Failed target',
|
||||
flashFail_other: 'Failed targets',
|
||||
toDrive: 'to {{description}} ({{name}})',
|
||||
toTarget_one: 'to {{num}} target',
|
||||
toTarget_other: 'to {{num}} targets',
|
||||
andFailTarget_one: 'and failed to be flashed to {{num}} target',
|
||||
andFailTarget_other: 'and failed to be flashed to {{num}} targets',
|
||||
succeedTo: '{{name}} was successfully flashed {{target}}',
|
||||
exitWhileFlashing:
|
||||
'You are currently flashing a drive. Closing Etcher may leave your drive in an unusable state.',
|
||||
looksLikeWindowsImage:
|
||||
'It looks like you are trying to burn a Windows image.\n\nUnlike other images, Windows images require special processing to be made bootable. We suggest you use a tool specially designed for this purpose, such as <a href="https://rufus.akeo.ie">Rufus</a> (Windows), <a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux), or Boot Camp Assistant (macOS).',
|
||||
image: 'image',
|
||||
drive: 'drive',
|
||||
missingPartitionTable:
|
||||
'It looks like this is not a bootable {{type}}.\n\nThe {{type}} does not appear to contain a partition table, and might not be recognized or bootable by your device.',
|
||||
largeDriveSize:
|
||||
"This is a large drive! Make sure it doesn't contain files that you want to keep.",
|
||||
systemDrive:
|
||||
'Selecting your system drive is dangerous and will erase your drive!',
|
||||
sourceDrive: 'Contains the image you chose to flash',
|
||||
noSpace:
|
||||
'Not enough space on the drive. Please insert larger one and try again.',
|
||||
genericFlashError:
|
||||
'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n{{error}}',
|
||||
validation:
|
||||
'The write has been completed successfully but Etcher detected potential corruption issues when reading the image back from the drive. \n\nPlease consider writing the image to a different drive.',
|
||||
openError:
|
||||
'Something went wrong while opening {{source}}.\n\nError: {{error}}',
|
||||
flashError: 'Something went wrong while writing {{image}} {{targets}}.',
|
||||
unplug:
|
||||
"Looks like Etcher lost access to the drive. Did it get unplugged accidentally?\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
|
||||
cannotWrite:
|
||||
'Looks like Etcher is not able to write to this location of the drive. This error is usually caused by a faulty drive, reader, or port. \n\nPlease try again with another drive, reader, or port.',
|
||||
childWriterDied:
|
||||
'The writer process ended unexpectedly. Please try again, and contact the Etcher team if the problem persists.',
|
||||
badProtocol: 'Only http:// and https:// URLs are supported.',
|
||||
},
|
||||
target: {
|
||||
selectTarget: 'Select target',
|
||||
plugTarget: 'Plug a target drive',
|
||||
targets: 'Targets',
|
||||
change: 'Change',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: 'Use Image URL',
|
||||
auth: 'Authentication',
|
||||
username: 'Enter username',
|
||||
password: 'Enter password',
|
||||
unsupportedProtocol: 'Unsupported protocol',
|
||||
windowsImage: 'Possible Windows image detected',
|
||||
partitionTable: 'Missing partition table',
|
||||
errorOpen: 'Error opening source',
|
||||
fromFile: 'Flash from file',
|
||||
fromURL: 'Flash from URL',
|
||||
clone: 'Clone drive',
|
||||
image: 'Image',
|
||||
name: 'Name: ',
|
||||
path: 'Path: ',
|
||||
selectSource: 'Select source',
|
||||
plugSource: 'Plug a source drive',
|
||||
osImages: 'OS Images',
|
||||
allFiles: 'All',
|
||||
enterValidURL: 'Enter a valid URL',
|
||||
},
|
||||
drives: {
|
||||
name: 'Name',
|
||||
size: 'Size',
|
||||
location: 'Location',
|
||||
find: '{{length}} found',
|
||||
select: 'Select {{select}}',
|
||||
showHidden: 'Show {{num}} hidden',
|
||||
systemDriveDanger:
|
||||
'Selecting your system drive is dangerous and will erase your drive!',
|
||||
openInBrowser: '`Etcher will open {{link}} in your browser`',
|
||||
changeTarget: 'Change target',
|
||||
largeDriveWarning: 'You are about to erase an unusually large drive',
|
||||
largeDriveWarningMsg:
|
||||
'Are you sure the selected drive is not a storage drive?',
|
||||
systemDriveWarning: "You are about to erase your computer's drives",
|
||||
systemDriveWarningMsg:
|
||||
'Are you sure you want to flash your system drive?',
|
||||
},
|
||||
flash: {
|
||||
another: 'Flash another',
|
||||
target: 'Target',
|
||||
location: 'Location',
|
||||
error: 'Error',
|
||||
flash: 'Flash',
|
||||
flashNow: 'Flash!',
|
||||
skip: 'Validation has been skipped',
|
||||
moreInfo: 'more info',
|
||||
speedTip:
|
||||
'The speed is calculated by dividing the image size by the flashing time.\nDisk images with ext partitions flash faster as we are able to skip unused parts.',
|
||||
speed: 'Effective speed: {{speed}} MB/s',
|
||||
speedShort: '{{speed}} MB/s',
|
||||
eta: 'ETA: {{eta}}',
|
||||
failedTarget: 'Failed targets',
|
||||
failedRetry: 'Retry failed targets',
|
||||
flashFailed: 'Flash Failed.',
|
||||
flashCompleted: 'Flash Completed!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting:
|
||||
'Anonymously report errors and usage statistics to balena.io',
|
||||
autoUpdate: 'Auto-updates enabled',
|
||||
settings: 'Settings',
|
||||
systemInformation: 'System Information',
|
||||
trimExtPartitions: 'Trim unallocated space on raw images (in ext-type partitions)',
|
||||
},
|
||||
menu: {
|
||||
edit: 'Edit',
|
||||
view: 'View',
|
||||
devTool: 'Toggle Developer Tools',
|
||||
window: 'Window',
|
||||
help: 'Help',
|
||||
pro: 'Etcher Pro',
|
||||
website: 'Etcher Website',
|
||||
issue: 'Report an issue',
|
||||
about: 'About Etcher',
|
||||
hide: 'Hide Etcher',
|
||||
hideOthers: 'Hide Others',
|
||||
unhide: 'Unhide All',
|
||||
quit: 'Quit Etcher',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
||||
152
lib/gui/app/i18n/zh-CN.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
ok: '好',
|
||||
cancel: '取消',
|
||||
continue: '继续',
|
||||
skip: '跳过',
|
||||
sure: '我确定',
|
||||
warning: '请注意!',
|
||||
attention: '请注意',
|
||||
failed: '失败',
|
||||
completed: '完毕',
|
||||
yesExit: '是的,可以退出',
|
||||
reallyExit: '真的要现在退出 Etcher 吗?',
|
||||
yesContinue: '是的,继续',
|
||||
progress: {
|
||||
starting: '正在启动……',
|
||||
decompressing: '正在解压……',
|
||||
flashing: '正在烧录……',
|
||||
finishing: '正在结束……',
|
||||
verifying: '正在验证……',
|
||||
failing: '失败……',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: '大小不推荐',
|
||||
tooSmall: '空间太小',
|
||||
locked: '被锁定',
|
||||
system: '系统盘',
|
||||
containsImage: '存放源镜像',
|
||||
largeDrive: '很大的磁盘',
|
||||
sourceLarger: '所选的镜像比目标盘大了 {{byte}} 比特。',
|
||||
flashSucceed_one: '烧录成功',
|
||||
flashSucceed_other: '烧录成功',
|
||||
flashFail_one: '烧录失败',
|
||||
flashFail_other: '烧录失败',
|
||||
toDrive: '到 {{description}} ({{name}})',
|
||||
toTarget_one: '到 {{num}} 个目标',
|
||||
toTarget_other: '到 {{num}} 个目标',
|
||||
andFailTarget_one: '并烧录失败了 {{num}} 个目标',
|
||||
andFailTarget_other: '并烧录失败了 {{num}} 个目标',
|
||||
succeedTo: '{{name}} 被成功烧录 {{target}}',
|
||||
exitWhileFlashing:
|
||||
'您当前正在刷机。 关闭 Etcher 可能会导致您的磁盘无法使用。',
|
||||
looksLikeWindowsImage:
|
||||
'看起来您正在尝试刻录 Windows 镜像。\n\n与其他镜像不同,Windows 镜像需要特殊处理才能使其可启动。 我们建议您使用专门为此目的设计的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
|
||||
image: '镜像',
|
||||
drive: '磁盘',
|
||||
missingPartitionTable:
|
||||
'看起来这不是一个可启动的{{type}}。\n\n这个{{type}}似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。',
|
||||
largeDriveSize: '这是个很大的磁盘!请检查并确认它不包含对您很重要的信息',
|
||||
systemDrive: '选择系统盘很危险,因为这将会删除你的系统',
|
||||
sourceDrive: '源镜像位于这个分区中',
|
||||
noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。',
|
||||
genericFlashError:
|
||||
'出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。\n{{error}}',
|
||||
validation:
|
||||
'写入已成功完成,但 Etcher 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。',
|
||||
openError: '打开 {{source}} 时出错。\n\n错误信息: {{error}}',
|
||||
flashError: '烧录 {{image}} {{targets}} 失败。',
|
||||
unplug:
|
||||
'看起来 Etcher 失去了对磁盘的连接。 它是不是被意外拔掉了?\n\n有时这个错误是因为读卡器出了故障。',
|
||||
cannotWrite:
|
||||
'看起来 Etcher 无法写入磁盘的这个位置。 此错误通常是由故障的磁盘、读取器或端口引起的。 \n\n请使用其他磁盘、读卡器或端口重试。',
|
||||
childWriterDied:
|
||||
'写入进程意外崩溃。请再试一次,如果问题仍然存在,请联系 Etcher 团队。',
|
||||
badProtocol: '仅支持 http:// 和 https:// 开头的网址。',
|
||||
},
|
||||
target: {
|
||||
selectTarget: '选择目标磁盘',
|
||||
plugTarget: '请插入目标磁盘',
|
||||
targets: '个目标',
|
||||
change: '更改',
|
||||
},
|
||||
menu: {
|
||||
edit: '编辑',
|
||||
view: '视图',
|
||||
devTool: '打开开发者工具',
|
||||
window: '窗口',
|
||||
help: '帮助',
|
||||
pro: 'Etcher 专业版',
|
||||
website: 'Etcher 的官网',
|
||||
issue: '提交一个 issue',
|
||||
about: '关于 Etcher',
|
||||
hide: '隐藏 Etcher',
|
||||
hideOthers: '隐藏其它窗口',
|
||||
unhide: '取消隐藏',
|
||||
quit: '退出 Etcher',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: '使用镜像网络地址',
|
||||
auth: '验证',
|
||||
username: '输入用户名',
|
||||
password: '输入密码',
|
||||
unsupportedProtocol: '不支持的协议',
|
||||
windowsImage: '这可能是 Windows 系统镜像',
|
||||
partitionTable: '找不到分区表',
|
||||
errorOpen: '打开源镜像时出错',
|
||||
fromFile: '从文件烧录',
|
||||
fromURL: '从在线地址烧录',
|
||||
clone: '克隆磁盘',
|
||||
image: '镜像信息',
|
||||
name: '名称:',
|
||||
path: '路径:',
|
||||
selectSource: '选择源',
|
||||
plugSource: '请插入源磁盘',
|
||||
osImages: '系统镜像格式',
|
||||
allFiles: '任何文件格式',
|
||||
enterValidURL: '请输入一个正确的地址',
|
||||
},
|
||||
drives: {
|
||||
name: '名称',
|
||||
size: '大小',
|
||||
location: '位置',
|
||||
find: '找到 {{length}} 个',
|
||||
select: '选定 {{select}}',
|
||||
showHidden: '显示 {{num}} 个隐藏的磁盘',
|
||||
systemDriveDanger: '选择系统盘很危险,因为这将会删除你的系统!',
|
||||
openInBrowser: 'Etcher 会在浏览器中打开 {{link}}',
|
||||
changeTarget: '改变目标',
|
||||
largeDriveWarning: '您即将擦除一个非常大的磁盘',
|
||||
largeDriveWarningMsg: '您确定所选磁盘不是存储磁盘吗?',
|
||||
systemDriveWarning: '您将要擦除系统盘',
|
||||
systemDriveWarningMsg: '您确定要烧录到系统盘吗?',
|
||||
},
|
||||
flash: {
|
||||
another: '烧录另一目标',
|
||||
target: '目标',
|
||||
location: '位置',
|
||||
error: '错误',
|
||||
flash: '烧录',
|
||||
flashNow: '现在烧录!',
|
||||
skip: '跳过了验证',
|
||||
moreInfo: '更多信息',
|
||||
speedTip:
|
||||
'通过将镜像大小除以烧录时间来计算速度。\n由于我们能够跳过未使用的部分,因此具有EXT分区的磁盘镜像烧录速度更快。',
|
||||
speed: '速度:{{speed}} MB/秒',
|
||||
speedShort: '{{speed}} MB/秒',
|
||||
eta: '预计还需要:{{eta}}',
|
||||
failedTarget: '失败的烧录目标',
|
||||
failedRetry: '重试烧录失败目标',
|
||||
flashFailed: '烧录失败。',
|
||||
flashCompleted: '烧录成功!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting: '匿名地向 balena.io 报告运行错误和使用统计',
|
||||
autoUpdate: '自动更新',
|
||||
settings: '软件设置',
|
||||
systemInformation: '系统信息',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
||||
152
lib/gui/app/i18n/zh-TW.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
ok: '好',
|
||||
cancel: '取消',
|
||||
continue: '繼續',
|
||||
skip: '跳過',
|
||||
sure: '我確定',
|
||||
warning: '請注意!',
|
||||
attention: '請注意',
|
||||
failed: '失敗',
|
||||
completed: '完畢',
|
||||
yesExit: '是的,可以退出',
|
||||
reallyExit: '真的要現在退出 Etcher 嗎?',
|
||||
yesContinue: '是的,繼續',
|
||||
progress: {
|
||||
starting: '正在啓動……',
|
||||
decompressing: '正在解壓……',
|
||||
flashing: '正在燒錄……',
|
||||
finishing: '正在結束……',
|
||||
verifying: '正在驗證……',
|
||||
failing: '失敗……',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: '大小不推薦',
|
||||
tooSmall: '空間太小',
|
||||
locked: '被鎖定',
|
||||
system: '系統盤',
|
||||
containsImage: '存放源鏡像',
|
||||
largeDrive: '很大的磁盤',
|
||||
sourceLarger: '所選的鏡像比目標盤大了 {{byte}} 比特。',
|
||||
flashSucceed_one: '燒錄成功',
|
||||
flashSucceed_other: '燒錄成功',
|
||||
flashFail_one: '燒錄失敗',
|
||||
flashFail_other: '燒錄失敗',
|
||||
toDrive: '到 {{description}} ({{name}})',
|
||||
toTarget_one: '到 {{num}} 個目標',
|
||||
toTarget_other: '到 {{num}} 個目標',
|
||||
andFailTarget_one: '並燒錄失敗了 {{num}} 個目標',
|
||||
andFailTarget_other: '並燒錄失敗了 {{num}} 個目標',
|
||||
succeedTo: '{{name}} 被成功燒錄 {{target}}',
|
||||
exitWhileFlashing:
|
||||
'您當前正在刷機。 關閉 Etcher 可能會導致您的磁盤無法使用。',
|
||||
looksLikeWindowsImage:
|
||||
'看起來您正在嘗試刻錄 Windows 鏡像。\n\n與其他鏡像不同,Windows 鏡像需要特殊處理才能使其可啓動。 我們建議您使用專門爲此目的設計的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
|
||||
image: '鏡像',
|
||||
drive: '磁盤',
|
||||
missingPartitionTable:
|
||||
'看起來這不是一個可啓動的{{type}}。\n\n這個{{type}}似乎不包含分區表,因此您的設備可能無法識別或無法正確啓動。',
|
||||
largeDriveSize: '這是個很大的磁盤!請檢查並確認它不包含對您很重要的信息',
|
||||
systemDrive: '選擇系統盤很危險,因爲這將會刪除你的系統',
|
||||
sourceDrive: '源鏡像位於這個分區中',
|
||||
noSpace: '磁盤空間不足。 請插入另一個較大的磁盤並重試。',
|
||||
genericFlashError:
|
||||
'出了點問題。如果源鏡像曾被壓縮過,請檢查它是否已損壞。\n{{error}}',
|
||||
validation:
|
||||
'寫入已成功完成,但 Etcher 在從磁盤讀取鏡像時檢測到潛在的損壞問題。 \n\n請考慮將鏡像寫入其他磁盤。',
|
||||
openError: '打開 {{source}} 時出錯。\n\n錯誤信息: {{error}}',
|
||||
flashError: '燒錄 {{image}} {{targets}} 失敗。',
|
||||
unplug:
|
||||
'看起來 Etcher 失去了對磁盤的連接。 它是不是被意外拔掉了?\n\n有時這個錯誤是因爲讀卡器出了故障。',
|
||||
cannotWrite:
|
||||
'看起來 Etcher 無法寫入磁盤的這個位置。 此錯誤通常是由故障的磁盤、讀取器或端口引起的。 \n\n請使用其他磁盤、讀卡器或端口重試。',
|
||||
childWriterDied:
|
||||
'寫入進程意外崩潰。請再試一次,如果問題仍然存在,請聯繫 Etcher 團隊。',
|
||||
badProtocol: '僅支持 http:// 和 https:// 開頭的網址。',
|
||||
},
|
||||
target: {
|
||||
selectTarget: '選擇目標磁盤',
|
||||
plugTarget: '請插入目標磁盤',
|
||||
targets: '個目標',
|
||||
change: '更改',
|
||||
},
|
||||
menu: {
|
||||
edit: '編輯',
|
||||
view: '視圖',
|
||||
devTool: '打開開發者工具',
|
||||
window: '窗口',
|
||||
help: '幫助',
|
||||
pro: 'Etcher 專業版',
|
||||
website: 'Etcher 的官網',
|
||||
issue: '提交一個 issue',
|
||||
about: '關於 Etcher',
|
||||
hide: '隱藏 Etcher',
|
||||
hideOthers: '隱藏其它窗口',
|
||||
unhide: '取消隱藏',
|
||||
quit: '退出 Etcher',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: '使用鏡像網絡地址',
|
||||
auth: '驗證',
|
||||
username: '輸入用戶名',
|
||||
password: '輸入密碼',
|
||||
unsupportedProtocol: '不支持的協議',
|
||||
windowsImage: '這可能是 Windows 系統鏡像',
|
||||
partitionTable: '找不到分區表',
|
||||
errorOpen: '打開源鏡像時出錯',
|
||||
fromFile: '從文件燒錄',
|
||||
fromURL: '從在線地址燒錄',
|
||||
clone: '克隆磁盤',
|
||||
image: '鏡像信息',
|
||||
name: '名稱:',
|
||||
path: '路徑:',
|
||||
selectSource: '選擇源',
|
||||
plugSource: '請插入源磁盤',
|
||||
osImages: '系統鏡像格式',
|
||||
allFiles: '任何文件格式',
|
||||
enterValidURL: '請輸入一個正確的地址',
|
||||
},
|
||||
drives: {
|
||||
name: '名稱',
|
||||
size: '大小',
|
||||
location: '位置',
|
||||
find: '找到 {{length}} 個',
|
||||
select: '選定 {{select}}',
|
||||
showHidden: '顯示 {{num}} 個隱藏的磁盤',
|
||||
systemDriveDanger: '選擇系統盤很危險,因爲這將會刪除你的系統!',
|
||||
openInBrowser: 'Etcher 會在瀏覽器中打開 {{link}}',
|
||||
changeTarget: '改變目標',
|
||||
largeDriveWarning: '您即將擦除一個非常大的磁盤',
|
||||
largeDriveWarningMsg: '您確定所選磁盤不是存儲磁盤嗎?',
|
||||
systemDriveWarning: '您將要擦除系統盤',
|
||||
systemDriveWarningMsg: '您確定要燒錄到系統盤嗎?',
|
||||
},
|
||||
flash: {
|
||||
another: '燒錄另一目標',
|
||||
target: '目標',
|
||||
location: '位置',
|
||||
error: '錯誤',
|
||||
flash: '燒錄',
|
||||
flashNow: '現在燒錄!',
|
||||
skip: '跳過了驗證',
|
||||
moreInfo: '更多信息',
|
||||
speedTip:
|
||||
'通過將鏡像大小除以燒錄時間來計算速度。\n由於我們能夠跳過未使用的部分,因此具有EXT分區的磁盤鏡像燒錄速度更快。',
|
||||
speed: '速度:{{speed}} MB/秒',
|
||||
speedShort: '{{speed}} MB/秒',
|
||||
eta: '預計還需要:{{eta}}',
|
||||
failedTarget: '失敗的燒錄目標',
|
||||
failedRetry: '重試燒錄失敗目標',
|
||||
flashFailed: '燒錄失敗。',
|
||||
flashCompleted: '燒錄成功!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting: '匿名地向 balena.io 報告運行錯誤和使用統計',
|
||||
autoUpdate: '自動更新',
|
||||
settings: '軟件設置',
|
||||
systemInformation: '系統信息',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
||||
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,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Etcher</title>
|
||||
<title>balenaEtcher</title>
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -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,9 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { bytesToMegabytes } from '../../../shared/units';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
@@ -45,6 +46,8 @@ export function isFlashing(): boolean {
|
||||
* start a flash process.
|
||||
*/
|
||||
export function setFlashingFlag() {
|
||||
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
|
||||
electron.ipcRenderer.invoke('disable-screensaver');
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASHING_FLAG,
|
||||
data: {},
|
||||
@@ -66,6 +69,8 @@ export function unsetFlashingFlag(results: {
|
||||
type: Actions.UNSET_FLASHING_FLAG,
|
||||
data: results,
|
||||
});
|
||||
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
|
||||
electron.ipcRenderer.invoke('enable-screensaver');
|
||||
}
|
||||
|
||||
export function setDevicePaths(devicePaths: string[]) {
|
||||
@@ -75,14 +80,29 @@ export function setDevicePaths(devicePaths: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function addFailedDevicePath(devicePath: string) {
|
||||
const failedDevicePathsSet = new Set(
|
||||
store.getState().toJS().failedDevicePaths,
|
||||
export function addFailedDeviceError({
|
||||
device,
|
||||
error,
|
||||
}: {
|
||||
device: DrivelistDrive;
|
||||
error: Error;
|
||||
}) {
|
||||
const failedDeviceErrorsMap = new Map(
|
||||
store.getState().toJS().failedDeviceErrors,
|
||||
);
|
||||
failedDevicePathsSet.add(devicePath);
|
||||
if (failedDeviceErrorsMap.has(device.device)) {
|
||||
// Only store the first error
|
||||
return;
|
||||
}
|
||||
failedDeviceErrorsMap.set(device.device, {
|
||||
description: device.description,
|
||||
device: device.device,
|
||||
devicePath: device.devicePath,
|
||||
...error,
|
||||
});
|
||||
store.dispatch({
|
||||
type: Actions.SET_FAILED_DEVICE_PATHS,
|
||||
data: Array.from(failedDevicePathsSet),
|
||||
type: Actions.SET_FAILED_DEVICE_ERRORS,
|
||||
data: Array.from(failedDeviceErrorsMap),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,39 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import { isSourceDrive } from '../../../shared/drive-constraints';
|
||||
import {
|
||||
DrivelistDrive,
|
||||
isSourceDrive,
|
||||
} from '../../../shared/drive-constraints';
|
||||
import { getDrives } from './available-drives';
|
||||
import { getSelectedDrives } from './selection-state';
|
||||
import * as settings from './settings';
|
||||
import { DEFAULT_STATE, observe } from './store';
|
||||
import { observe, store } from './store';
|
||||
|
||||
const leds: Map<string, RGBLed> = new Map();
|
||||
|
||||
function setLeds(
|
||||
drivesPaths: Set<string>,
|
||||
colorOrAnimation: Color | AnimationFunction,
|
||||
frequency?: number,
|
||||
) {
|
||||
for (const path of drivesPaths) {
|
||||
const led = leds.get(path);
|
||||
if (led) {
|
||||
if (Array.isArray(colorOrAnimation)) {
|
||||
led.setStaticColor(colorOrAnimation);
|
||||
} else {
|
||||
led.setAnimation(colorOrAnimation, frequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const red: Color = [1, 0, 0];
|
||||
const green: Color = [0, 1, 0];
|
||||
const blue: Color = [0, 0, 1];
|
||||
const white: Color = [1, 1, 1];
|
||||
const black: Color = [0, 0, 0];
|
||||
const purple: Color = [0.5, 0, 0.5];
|
||||
const animator = new Animator([], 10);
|
||||
|
||||
function createAnimationFunction(
|
||||
intensityFunction: (t: number) => number,
|
||||
@@ -54,21 +35,39 @@ function createAnimationFunction(
|
||||
): AnimationFunction {
|
||||
return (t: number): Color => {
|
||||
const intensity = intensityFunction(t);
|
||||
return color.map((v) => v * intensity) as Color;
|
||||
return color.map((v: number) => v * intensity) as Color;
|
||||
};
|
||||
}
|
||||
|
||||
function blink(t: number) {
|
||||
return Math.floor(t / 1000) % 2;
|
||||
return Math.floor(t) % 2;
|
||||
}
|
||||
|
||||
function breathe(t: number) {
|
||||
return (1 + Math.sin(t / 1000)) / 2;
|
||||
function one(_t: number) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const breatheBlue = createAnimationFunction(breathe, blue);
|
||||
const blinkGreen = createAnimationFunction(blink, green);
|
||||
const blinkPurple = createAnimationFunction(blink, purple);
|
||||
type LEDColors = {
|
||||
green: Color;
|
||||
purple: Color;
|
||||
red: Color;
|
||||
blue: Color;
|
||||
white: Color;
|
||||
black: Color;
|
||||
};
|
||||
|
||||
type LEDAnimationFunctions = {
|
||||
blinkGreen: AnimationFunction;
|
||||
blinkPurple: AnimationFunction;
|
||||
staticRed: AnimationFunction;
|
||||
staticGreen: AnimationFunction;
|
||||
staticBlue: AnimationFunction;
|
||||
staticWhite: AnimationFunction;
|
||||
staticBlack: AnimationFunction;
|
||||
};
|
||||
|
||||
let ledColors: LEDColors;
|
||||
let ledAnimationFunctions: LEDAnimationFunctions;
|
||||
|
||||
interface LedsState {
|
||||
step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||
@@ -78,6 +77,17 @@ interface LedsState {
|
||||
failedDrives: string[];
|
||||
}
|
||||
|
||||
function setLeds(animation: AnimationFunction, drivesPaths: Set<string>) {
|
||||
const rgbLeds: RGBLed[] = [];
|
||||
for (const path of drivesPaths) {
|
||||
const led = leds.get(path);
|
||||
if (led) {
|
||||
rgbLeds.push(led);
|
||||
}
|
||||
}
|
||||
return { animation, rgbLeds };
|
||||
}
|
||||
|
||||
// Source slot (1st slot): behaves as a target unless it is chosen as source
|
||||
// No drive: black
|
||||
// Drive plugged: blue - on
|
||||
@@ -108,6 +118,7 @@ export function updateLeds({
|
||||
// Remove selected devices from plugged set
|
||||
for (const d of selectedOk) {
|
||||
plugged.delete(d);
|
||||
unplugged.delete(d);
|
||||
}
|
||||
|
||||
// Remove plugged devices from unplugged set
|
||||
@@ -120,79 +131,98 @@ export function updateLeds({
|
||||
selectedOk.delete(d);
|
||||
}
|
||||
|
||||
const mapping: Array<{
|
||||
animation: AnimationFunction;
|
||||
rgbLeds: RGBLed[];
|
||||
}> = [];
|
||||
// Handle source slot
|
||||
if (sourceDrive !== undefined) {
|
||||
if (unplugged.has(sourceDrive)) {
|
||||
unplugged.delete(sourceDrive);
|
||||
// TODO
|
||||
setLeds(new Set([sourceDrive]), breatheBlue, 2);
|
||||
} else if (plugged.has(sourceDrive)) {
|
||||
if (plugged.has(sourceDrive)) {
|
||||
plugged.delete(sourceDrive);
|
||||
setLeds(new Set([sourceDrive]), blue);
|
||||
mapping.push(
|
||||
setLeds(ledAnimationFunctions.staticBlue, new Set([sourceDrive])),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (step === 'main') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, white);
|
||||
setLeds(selectedFailed, white);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticWhite,
|
||||
new Set([...selectedOk, ...selectedFailed]),
|
||||
),
|
||||
);
|
||||
} else if (step === 'flashing') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, blinkPurple, 2);
|
||||
setLeds(selectedFailed, red);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.blinkPurple, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
} else if (step === 'verifying') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, blinkGreen, 2);
|
||||
setLeds(selectedFailed, red);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.blinkGreen, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
} else if (step === 'finish') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, green);
|
||||
setLeds(selectedFailed, red);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.staticGreen, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface DeviceFromState {
|
||||
devicePath?: string;
|
||||
device: string;
|
||||
animator.mapping = mapping;
|
||||
}
|
||||
|
||||
let ledsState: LedsState | undefined;
|
||||
|
||||
function stateObserver(state: typeof DEFAULT_STATE) {
|
||||
const s = state.toJS();
|
||||
function stateObserver() {
|
||||
const s = store.getState().toJS();
|
||||
let step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||
if (s.isFlashing) {
|
||||
step = s.flashState.type;
|
||||
} else {
|
||||
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
|
||||
}
|
||||
const availableDrives = s.availableDrives.filter(
|
||||
(d: DeviceFromState) => d.devicePath,
|
||||
const availableDrives = getDrives().filter(
|
||||
(d: DrivelistDrive) => d.devicePath,
|
||||
);
|
||||
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
|
||||
isSourceDrive(d, s.selection.image),
|
||||
)[0]?.devicePath;
|
||||
const availableDrivesPaths = availableDrives.map(
|
||||
(d: DeviceFromState) => d.devicePath,
|
||||
(d: DrivelistDrive) => d.devicePath,
|
||||
);
|
||||
let selectedDrivesPaths: string[];
|
||||
if (step === 'main') {
|
||||
selectedDrivesPaths = availableDrives
|
||||
.filter((d: DrivelistDrive) => s.selection.devices.includes(d.device))
|
||||
.map((d: DrivelistDrive) => d.devicePath);
|
||||
selectedDrivesPaths = getSelectedDrives()
|
||||
.filter((drive) => drive.devicePath !== null)
|
||||
.map((drive) => drive.devicePath) as string[];
|
||||
} else {
|
||||
selectedDrivesPaths = s.devicePaths;
|
||||
}
|
||||
const failedDevicePaths = s.failedDeviceErrors.map(
|
||||
([, { devicePath }]: [string, { devicePath: string }]) => devicePath,
|
||||
);
|
||||
const newLedsState = {
|
||||
step,
|
||||
sourceDrive: sourceDrivePath,
|
||||
availableDrives: availableDrivesPaths,
|
||||
selectedDrives: selectedDrivesPaths,
|
||||
failedDrives: s.failedDevicePaths,
|
||||
};
|
||||
failedDrives: failedDevicePaths,
|
||||
} as LedsState;
|
||||
if (!_.isEqual(newLedsState, ledsState)) {
|
||||
updateLeds(newLedsState);
|
||||
ledsState = newLedsState;
|
||||
@@ -215,6 +245,16 @@ export async function init(): Promise<void> {
|
||||
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
|
||||
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
|
||||
}
|
||||
ledColors = (await settings.get('ledColors')) || {};
|
||||
ledAnimationFunctions = {
|
||||
blinkGreen: createAnimationFunction(blink, ledColors['green']),
|
||||
blinkPurple: createAnimationFunction(blink, ledColors['purple']),
|
||||
staticRed: createAnimationFunction(one, ledColors['red']),
|
||||
staticGreen: createAnimationFunction(one, ledColors['green']),
|
||||
staticBlue: createAnimationFunction(one, ledColors['blue']),
|
||||
staticWhite: createAnimationFunction(one, ledColors['white']),
|
||||
staticBlack: createAnimationFunction(one, ledColors['black']),
|
||||
};
|
||||
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
@@ -14,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||
|
||||
import * as availableDrives from './available-drives';
|
||||
import { Actions, store } from './store';
|
||||
@@ -24,7 +25,7 @@ import { Actions, store } from './store';
|
||||
*/
|
||||
export function selectDrive(driveDevice: string) {
|
||||
store.dispatch({
|
||||
type: Actions.SELECT_DRIVE,
|
||||
type: Actions.SELECT_TARGET,
|
||||
data: driveDevice,
|
||||
});
|
||||
}
|
||||
@@ -40,10 +41,10 @@ export function toggleDrive(driveDevice: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function selectImage(image: any) {
|
||||
export function selectSource(source: SourceMetadata) {
|
||||
store.dispatch({
|
||||
type: Actions.SELECT_IMAGE,
|
||||
data: image,
|
||||
type: Actions.SELECT_SOURCE,
|
||||
data: source,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,50 +58,18 @@ export function getSelectedDevices(): string[] {
|
||||
/**
|
||||
* @summary Get all selected drive objects
|
||||
*/
|
||||
export function getSelectedDrives(): any[] {
|
||||
const drives = availableDrives.getDrives();
|
||||
return _.map(getSelectedDevices(), (device) => {
|
||||
return _.find(drives, { device });
|
||||
});
|
||||
export function getSelectedDrives(): DrivelistDrive[] {
|
||||
const selectedDevices = getSelectedDevices();
|
||||
return availableDrives
|
||||
.getDrives()
|
||||
.filter((drive) => selectedDevices.includes(drive.device));
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the selected image
|
||||
*/
|
||||
export function getImage() {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image']);
|
||||
}
|
||||
|
||||
export function getImagePath(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'path']);
|
||||
}
|
||||
|
||||
export function getImageSize(): number {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'size']);
|
||||
}
|
||||
|
||||
export function getImageUrl(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'url']);
|
||||
}
|
||||
|
||||
export function getImageName(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'name']);
|
||||
}
|
||||
|
||||
export function getImageLogo(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'logo']);
|
||||
}
|
||||
|
||||
export function getImageSupportUrl(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'supportUrl']);
|
||||
}
|
||||
|
||||
export function getImageRecommendedDriveSize(): number {
|
||||
return _.get(store.getState().toJS(), [
|
||||
'selection',
|
||||
'image',
|
||||
'recommendedDriveSize',
|
||||
]);
|
||||
export function getImage(): SourceMetadata | undefined {
|
||||
return store.getState().toJS().selection.image;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +83,7 @@ export function hasDrive(): boolean {
|
||||
* @summary Check if there is a selected image
|
||||
*/
|
||||
export function hasImage(): boolean {
|
||||
return Boolean(getImage());
|
||||
return getImage() !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,20 +91,20 @@ export function hasImage(): boolean {
|
||||
*/
|
||||
export function deselectDrive(driveDevice: string) {
|
||||
store.dispatch({
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: driveDevice,
|
||||
});
|
||||
}
|
||||
|
||||
export function deselectImage() {
|
||||
store.dispatch({
|
||||
type: Actions.DESELECT_IMAGE,
|
||||
type: Actions.DESELECT_SOURCE,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
export function deselectAllDrives() {
|
||||
_.each(getSelectedDevices(), deselectDrive);
|
||||
getSelectedDevices().forEach(deselectDrive);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,5 +124,5 @@ export function isDriveSelected(driveDevice: string) {
|
||||
}
|
||||
|
||||
const selectedDriveDevices = getSelectedDevices();
|
||||
return _.includes(selectedDriveDevices, driveDevice);
|
||||
return selectedDriveDevices.includes(driveDevice);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings');
|
||||
|
||||
const JSON_INDENT = 2;
|
||||
|
||||
export const DEFAULT_WIDTH = 800;
|
||||
export const DEFAULT_HEIGHT = 480;
|
||||
|
||||
/**
|
||||
* @summary Userdata directory path
|
||||
* @description
|
||||
@@ -35,12 +38,12 @@ const JSON_INDENT = 2;
|
||||
* - `~/Library/Application Support/etcher` on macOS
|
||||
* See https://electronjs.org/docs/api/app#appgetpathname
|
||||
*
|
||||
* NOTE: The ternary is due to this module being loaded both,
|
||||
* Electron's main process and renderer process
|
||||
* NOTE: We use the remote property when this module
|
||||
* is loaded in the Electron's renderer process
|
||||
*/
|
||||
const USER_DATA_DIR = electron.app
|
||||
? electron.app.getPath('userData')
|
||||
: electron.remote.app.getPath('userData');
|
||||
const app = electron.app || electron.remote.app;
|
||||
|
||||
const USER_DATA_DIR = app.getPath('userData');
|
||||
|
||||
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
@@ -48,7 +51,7 @@ async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
||||
let contents = '{}';
|
||||
try {
|
||||
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// noop
|
||||
}
|
||||
try {
|
||||
@@ -74,9 +77,7 @@ export async function writeConfigFile(
|
||||
|
||||
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true,
|
||||
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
|
||||
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
|
||||
desktopNotifications: true,
|
||||
autoBlockmapping: true,
|
||||
decompressFirst: true,
|
||||
@@ -86,22 +87,24 @@ const settings = _.cloneDeep(DEFAULT_SETTINGS);
|
||||
|
||||
async function load(): Promise<void> {
|
||||
debug('load');
|
||||
// Use exports.readAll() so it can be mocked in tests
|
||||
const loadedSettings = await exports.readAll();
|
||||
const loadedSettings = await readAll();
|
||||
_.assign(settings, loadedSettings);
|
||||
}
|
||||
|
||||
const loaded = load();
|
||||
|
||||
export async function set(key: string, value: any): Promise<void> {
|
||||
export async function set(
|
||||
key: string,
|
||||
value: any,
|
||||
writeConfigFileFn = writeConfigFile,
|
||||
): Promise<void> {
|
||||
debug('set', key, value);
|
||||
await loaded;
|
||||
const previousValue = settings[key];
|
||||
settings[key] = value;
|
||||
try {
|
||||
// Use exports.writeConfigFile() so it can be mocked in tests
|
||||
await exports.writeConfigFile(CONFIG_PATH, settings);
|
||||
} catch (error) {
|
||||
await writeConfigFileFn(CONFIG_PATH, settings);
|
||||
} catch (error: any) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
throw error;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import * as Immutable from 'immutable';
|
||||
import * as _ from 'lodash';
|
||||
import { basename } from 'path';
|
||||
import * as redux from 'redux';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
@@ -62,7 +63,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
|
||||
},
|
||||
isFlashing: false,
|
||||
devicePaths: [],
|
||||
failedDevicePaths: [],
|
||||
failedDeviceErrors: [],
|
||||
flashResults: {},
|
||||
flashState: {
|
||||
active: 0,
|
||||
@@ -79,16 +80,16 @@ export const DEFAULT_STATE = Immutable.fromJS({
|
||||
*/
|
||||
export enum Actions {
|
||||
SET_DEVICE_PATHS,
|
||||
SET_FAILED_DEVICE_PATHS,
|
||||
SET_AVAILABLE_DRIVES,
|
||||
SET_FAILED_DEVICE_ERRORS,
|
||||
SET_AVAILABLE_TARGETS,
|
||||
SET_FLASH_STATE,
|
||||
RESET_FLASH_STATE,
|
||||
SET_FLASHING_FLAG,
|
||||
UNSET_FLASHING_FLAG,
|
||||
SELECT_DRIVE,
|
||||
SELECT_IMAGE,
|
||||
DESELECT_DRIVE,
|
||||
DESELECT_IMAGE,
|
||||
SELECT_TARGET,
|
||||
SELECT_SOURCE,
|
||||
DESELECT_TARGET,
|
||||
DESELECT_SOURCE,
|
||||
SET_APPLICATION_SESSION_UUID,
|
||||
SET_FLASHING_WORKFLOW_UUID,
|
||||
}
|
||||
@@ -116,7 +117,7 @@ function storeReducer(
|
||||
action: Action,
|
||||
): typeof DEFAULT_STATE {
|
||||
switch (action.type) {
|
||||
case Actions.SET_AVAILABLE_DRIVES: {
|
||||
case Actions.SET_AVAILABLE_TARGETS: {
|
||||
// Type: action.data : Array<DriveObject>
|
||||
|
||||
if (!action.data) {
|
||||
@@ -133,11 +134,16 @@ function storeReducer(
|
||||
});
|
||||
}
|
||||
|
||||
// Drives order is a list of devicePaths
|
||||
const drivesOrder = settings.getSync('drivesOrder') ?? [];
|
||||
|
||||
drives = _.sortBy(drives, [
|
||||
// System drives last
|
||||
(d) => !!d.isSystem,
|
||||
// Devices with no devicePath first (usbboot)
|
||||
(d) => !!d.devicePath,
|
||||
// Sort as defined in the drivesOrder setting if there is one (only for Linux with udev)
|
||||
(d) => drivesOrder.indexOf(basename(d.devicePath || '')),
|
||||
// Then sort by devicePath (only available on Linux with udev) or device
|
||||
(d) => d.devicePath || d.device,
|
||||
]);
|
||||
@@ -158,7 +164,7 @@ function storeReducer(
|
||||
) {
|
||||
// Deselect this drive gone from availableDrives
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@@ -169,7 +175,7 @@ function storeReducer(
|
||||
);
|
||||
|
||||
const shouldAutoselectAll = Boolean(
|
||||
settings.getSync('disableExplicitDriveSelection'),
|
||||
settings.getSync('autoSelectAllDrives'),
|
||||
);
|
||||
const AUTOSELECT_DRIVE_COUNT = 1;
|
||||
const nonStaleSelectedDevices = nonStaleNewState
|
||||
@@ -191,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,
|
||||
});
|
||||
},
|
||||
@@ -269,7 +270,7 @@ function storeReducer(
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'))
|
||||
.set('flashResults', DEFAULT_STATE.get('flashResults'))
|
||||
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
|
||||
.set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths'))
|
||||
.set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
|
||||
.set(
|
||||
'lastAverageFlashingSpeed',
|
||||
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
|
||||
@@ -295,6 +296,7 @@ function storeReducer(
|
||||
|
||||
_.defaults(action.data, {
|
||||
cancelled: false,
|
||||
skip: false,
|
||||
});
|
||||
|
||||
if (!_.isBoolean(action.data.cancelled)) {
|
||||
@@ -335,13 +337,19 @@ function storeReducer(
|
||||
);
|
||||
}
|
||||
|
||||
if (action.data.skip) {
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashResults', Immutable.fromJS(action.data));
|
||||
}
|
||||
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashResults', Immutable.fromJS(action.data))
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'));
|
||||
}
|
||||
|
||||
case Actions.SELECT_DRIVE: {
|
||||
case Actions.SELECT_TARGET: {
|
||||
// Type: action.data : String
|
||||
|
||||
const device = action.data;
|
||||
@@ -391,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({
|
||||
@@ -456,7 +466,7 @@ function storeReducer(
|
||||
!constraints.isDriveSizeRecommended(drive, action.data)
|
||||
) {
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@@ -467,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) {
|
||||
@@ -491,7 +501,7 @@ function storeReducer(
|
||||
);
|
||||
}
|
||||
|
||||
case Actions.DESELECT_IMAGE: {
|
||||
case Actions.DESELECT_SOURCE: {
|
||||
return state.deleteIn(['selection', 'image']);
|
||||
}
|
||||
|
||||
@@ -507,8 +517,8 @@ function storeReducer(
|
||||
return state.set('devicePaths', action.data);
|
||||
}
|
||||
|
||||
case Actions.SET_FAILED_DEVICE_PATHS: {
|
||||
return state.set('failedDevicePaths', action.data);
|
||||
case Actions.SET_FAILED_DEVICE_ERRORS: {
|
||||
return state.set('failedDeviceErrors', action.data);
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
@@ -18,7 +18,7 @@ import * as _ from 'lodash';
|
||||
import * as resinCorvus from 'resin-corvus/browser';
|
||||
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import { getConfig, hasProps } from '../../../shared/utils';
|
||||
import { getConfig } from '../../../shared/utils';
|
||||
import * as settings from '../models/settings';
|
||||
import { store } from '../models/store';
|
||||
|
||||
@@ -55,7 +55,8 @@ async function initConfig() {
|
||||
await installCorvus();
|
||||
let validatedConfig = null;
|
||||
try {
|
||||
const config = await getConfig();
|
||||
const configUrl = await settings.get('configUrl');
|
||||
const config = await getConfig(configUrl);
|
||||
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
|
||||
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
|
||||
if (isClientEligible(mixpanelSample)) {
|
||||
@@ -88,7 +89,7 @@ function validateMixpanelConfig(config: {
|
||||
const mixpanelConfig = {
|
||||
api_host: 'https://api.mixpanel.com',
|
||||
};
|
||||
if (hasProps(config, ['HTTP_PROTOCOL', 'api_host'])) {
|
||||
if (config.HTTP_PROTOCOL !== undefined && config.api_host !== undefined) {
|
||||
mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`;
|
||||
}
|
||||
return mixpanelConfig;
|
||||
@@ -101,10 +102,9 @@ function validateMixpanelConfig(config: {
|
||||
* This function sends the debug message to product analytics services.
|
||||
*/
|
||||
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
|
||||
const {
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
} = store.getState().toJS();
|
||||
const { applicationSessionUuid, flashingWorkflowUuid } = store
|
||||
.getState()
|
||||
.toJS();
|
||||
resinCorvus.logEvent(message, {
|
||||
...data,
|
||||
sample: mixpanelSample,
|
||||
|
||||
@@ -15,10 +15,15 @@
|
||||
*/
|
||||
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import {
|
||||
Adapter,
|
||||
BlockDeviceAdapter,
|
||||
UsbbootDeviceAdapter,
|
||||
} from 'etcher-sdk/build/scanner/adapters';
|
||||
import { geteuid, platform } from 'process';
|
||||
|
||||
const adapters: sdk.scanner.adapters.Adapter[] = [
|
||||
new sdk.scanner.adapters.BlockDeviceAdapter({
|
||||
const adapters: Adapter[] = [
|
||||
new BlockDeviceAdapter({
|
||||
includeSystemDrives: () => true,
|
||||
}),
|
||||
];
|
||||
@@ -26,14 +31,15 @@ const adapters: sdk.scanner.adapters.Adapter[] = [
|
||||
// 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,7 +24,8 @@ import * as path from 'path';
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as permissions from '../../../shared/permissions';
|
||||
import { SourceOptions } from '../components/source-selector/source-selector';
|
||||
import { getAppPath } from '../../../shared/utils';
|
||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||
import * as flashState from '../models/flash-state';
|
||||
import * as selectionState from '../models/selection-state';
|
||||
import * as settings from '../models/settings';
|
||||
@@ -93,7 +93,7 @@ function terminateServer() {
|
||||
}
|
||||
|
||||
function writerArgv(): string[] {
|
||||
let entryPoint = electron.remote.app.getAppPath();
|
||||
let entryPoint = path.join(getAppPath(), 'generated', 'child-writer.js');
|
||||
// AppImages run over FUSE, so the files inside the mount point
|
||||
// can only be accessed by the user that mounted the AppImage.
|
||||
// This means we can't re-spawn Etcher as root from the same
|
||||
@@ -127,29 +127,27 @@ function writerEnv() {
|
||||
}
|
||||
|
||||
interface FlashResults {
|
||||
skip?: boolean;
|
||||
cancelled?: boolean;
|
||||
results?: {
|
||||
bytesWritten: number;
|
||||
devices: {
|
||||
failed: number;
|
||||
successful: number;
|
||||
};
|
||||
errors: Error[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Perform write operation
|
||||
*
|
||||
* @description
|
||||
* This function is extracted for testing purposes.
|
||||
*/
|
||||
export async function performWrite(
|
||||
image: string,
|
||||
async function performWrite(
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
onProgress: sdk.multiWrite.OnProgressFunction,
|
||||
source: SourceOptions,
|
||||
): Promise<{ cancelled?: boolean }> {
|
||||
let cancelled = false;
|
||||
let skip = false;
|
||||
ipc.serve();
|
||||
const {
|
||||
unmountOnSuccess,
|
||||
validateWriteOnSuccess,
|
||||
autoBlockmapping,
|
||||
decompressFirst,
|
||||
} = await settings.getAll();
|
||||
const { autoBlockmapping, decompressFirst } = await settings.getAll();
|
||||
return await new Promise((resolve, reject) => {
|
||||
ipc.server.on('error', (error) => {
|
||||
terminateServer();
|
||||
@@ -168,22 +166,22 @@ export async function performWrite(
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess,
|
||||
validateWriteOnSuccess,
|
||||
};
|
||||
|
||||
ipc.server.on('fail', ({ device, error }) => {
|
||||
if (device.devicePath) {
|
||||
flashState.addFailedDevicePath(device.devicePath);
|
||||
flashState.addFailedDeviceError({ device, error });
|
||||
}
|
||||
handleErrorLogging(error, analyticsData);
|
||||
});
|
||||
|
||||
ipc.server.on('done', (event) => {
|
||||
event.results.errors = _.map(event.results.errors, (data) => {
|
||||
return errors.fromJSON(data);
|
||||
});
|
||||
_.merge(flashResults, event);
|
||||
event.results.errors = event.results.errors.map(
|
||||
(data: Dictionary<any> & { message: string }) => {
|
||||
return errors.fromJSON(data);
|
||||
},
|
||||
);
|
||||
flashResults.results = event.results;
|
||||
});
|
||||
|
||||
ipc.server.on('abort', () => {
|
||||
@@ -191,17 +189,19 @@ export async function performWrite(
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
ipc.server.on('skip', () => {
|
||||
terminateServer();
|
||||
skip = true;
|
||||
});
|
||||
|
||||
ipc.server.on('state', onProgress);
|
||||
|
||||
ipc.server.on('ready', (_data, socket) => {
|
||||
ipc.server.emit(socket, 'write', {
|
||||
imagePath: image,
|
||||
image,
|
||||
destinations: drives,
|
||||
source,
|
||||
SourceType: source.SourceType.name,
|
||||
validateWriteOnSuccess,
|
||||
SourceType: image.SourceType.name,
|
||||
autoBlockmapping,
|
||||
unmountOnSuccess,
|
||||
decompressFirst,
|
||||
});
|
||||
});
|
||||
@@ -209,7 +209,7 @@ export async function performWrite(
|
||||
const argv = writerArgv();
|
||||
|
||||
ipc.server.on('start', async () => {
|
||||
console.log(`Elevating command: ${_.join(argv, ' ')}`);
|
||||
console.log(`Elevating command: ${argv.join(' ')}`);
|
||||
const env = writerEnv();
|
||||
try {
|
||||
const results = await permissions.elevateCommand(argv, {
|
||||
@@ -217,7 +217,8 @@ export async function performWrite(
|
||||
environment: env,
|
||||
});
|
||||
flashResults.cancelled = cancelled || results.cancelled;
|
||||
} catch (error) {
|
||||
flashResults.skip = skip;
|
||||
} catch (error: any) {
|
||||
// This happens when the child is killed using SIGKILL
|
||||
const SIGKILL_EXIT_CODE = 137;
|
||||
if (error.code === SIGKILL_EXIT_CODE) {
|
||||
@@ -230,17 +231,17 @@ export async function performWrite(
|
||||
}
|
||||
console.log('Flash results', flashResults);
|
||||
|
||||
// This likely means the child died halfway through
|
||||
// The flash wasn't cancelled and we didn't get a 'done' event
|
||||
if (
|
||||
!flashResults.cancelled &&
|
||||
!_.get(flashResults, ['results', 'bytesWritten'])
|
||||
!flashResults.skip &&
|
||||
flashResults.results === undefined
|
||||
) {
|
||||
reject(
|
||||
errors.createUserError({
|
||||
title: 'The writer process ended unexpectedly',
|
||||
description:
|
||||
'Please try again, and contact the Etcher team if the problem persists',
|
||||
code: 'ECHILDDIED',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
@@ -258,15 +259,16 @@ export async function performWrite(
|
||||
* @summary Flash an image to drives
|
||||
*/
|
||||
export async function flash(
|
||||
image: string,
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
source: SourceOptions,
|
||||
// This function is a parameter so it can be mocked in tests
|
||||
write = performWrite,
|
||||
): Promise<void> {
|
||||
if (flashState.isFlashing()) {
|
||||
throw new Error('There is already a flash in progress');
|
||||
}
|
||||
|
||||
flashState.setFlashingFlag();
|
||||
await flashState.setFlashingFlag();
|
||||
flashState.setDevicePaths(
|
||||
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
|
||||
);
|
||||
@@ -278,26 +280,20 @@ export async function flash(
|
||||
uuid: flashState.getFlashUuid(),
|
||||
status: 'started',
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
|
||||
};
|
||||
|
||||
analytics.logEvent('Flash', analyticsData);
|
||||
|
||||
try {
|
||||
// Using it from exports so it can be mocked during tests
|
||||
const result = await exports.performWrite(
|
||||
image,
|
||||
drives,
|
||||
flashState.setProgressState,
|
||||
source,
|
||||
);
|
||||
flashState.unsetFlashingFlag(result);
|
||||
} catch (error) {
|
||||
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
|
||||
const result = await write(image, drives, flashState.setProgressState);
|
||||
await flashState.unsetFlashingFlag(result);
|
||||
} catch (error: any) {
|
||||
await flashState.unsetFlashingFlag({
|
||||
cancelled: false,
|
||||
errorCode: error.code,
|
||||
});
|
||||
windowProgress.clear();
|
||||
let { results } = flashState.getFlashResults();
|
||||
results = results || {};
|
||||
const { results = {} } = flashState.getFlashResults();
|
||||
const eventData = {
|
||||
...analyticsData,
|
||||
errors: results.errors,
|
||||
@@ -316,7 +312,7 @@ export async function flash(
|
||||
};
|
||||
analytics.logEvent('Elevation cancelled', eventData);
|
||||
} else {
|
||||
const { results } = flashState.getFlashResults();
|
||||
const { results = {} } = flashState.getFlashResults();
|
||||
const eventData = {
|
||||
...analyticsData,
|
||||
errors: results.errors,
|
||||
@@ -332,17 +328,16 @@ export async function flash(
|
||||
/**
|
||||
* @summary Cancel write operation
|
||||
*/
|
||||
export async function cancel() {
|
||||
export async function cancel(type: string) {
|
||||
const status = type.toLowerCase();
|
||||
const drives = selectionState.getSelectedDevices();
|
||||
const analyticsData = {
|
||||
image: selectionState.getImagePath(),
|
||||
image: selectionState.getImage()?.path,
|
||||
drives,
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
|
||||
status: 'cancel',
|
||||
status,
|
||||
};
|
||||
analytics.logEvent('Cancel', analyticsData);
|
||||
|
||||
@@ -352,9 +347,9 @@ export async function cancel() {
|
||||
// @ts-ignore (no Server.sockets in @types/node-ipc)
|
||||
const [socket] = ipc.server.sockets;
|
||||
if (socket !== undefined) {
|
||||
ipc.server.emit(socket, 'cancel');
|
||||
ipc.server.emit(socket, status);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { bytesToClosestUnit } from '../../../shared/units';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export interface FlashState {
|
||||
active: number;
|
||||
@@ -34,36 +35,45 @@ export function fromFlashState({
|
||||
position?: string;
|
||||
} {
|
||||
if (type === undefined) {
|
||||
return { status: 'Starting...' };
|
||||
return { status: i18next.t('progress.starting') };
|
||||
} else if (type === 'decompressing') {
|
||||
if (percentage == null) {
|
||||
return { status: 'Decompressing...' };
|
||||
return { status: i18next.t('progress.decompressing') };
|
||||
} else {
|
||||
return { position: `${percentage}%`, status: 'Decompressing...' };
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.decompressing'),
|
||||
};
|
||||
}
|
||||
} else if (type === 'flashing') {
|
||||
if (percentage != null) {
|
||||
if (percentage < 100) {
|
||||
return { position: `${percentage}%`, status: 'Flashing...' };
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.flashing'),
|
||||
};
|
||||
} else {
|
||||
return { status: 'Finishing...' };
|
||||
return { status: i18next.t('progress.finishing') };
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: 'Flashing...',
|
||||
position: `${bytesToClosestUnit(position)}`,
|
||||
status: i18next.t('progress.flashing'),
|
||||
position: `${position ? prettyBytes(position) : ''}`,
|
||||
};
|
||||
}
|
||||
} else if (type === 'verifying') {
|
||||
if (percentage == null) {
|
||||
return { status: 'Validating...' };
|
||||
return { status: i18next.t('progress.verifying') };
|
||||
} else if (percentage < 100) {
|
||||
return { position: `${percentage}%`, status: 'Validating...' };
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.verifying'),
|
||||
};
|
||||
} else {
|
||||
return { status: 'Finishing...' };
|
||||
return { status: i18next.t('progress.finishing') };
|
||||
}
|
||||
}
|
||||
return { status: 'Failed' };
|
||||
return { status: i18next.t('progress.failing') };
|
||||
}
|
||||
|
||||
export function titleFromFlashState(
|
||||
|
||||
@@ -18,7 +18,21 @@ import * as electron from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as settings from '../../../gui/app/models/settings';
|
||||
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
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 +41,7 @@ import { SUPPORTED_EXTENSIONS } 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
|
||||
@@ -39,11 +54,11 @@ export async function selectImage(): Promise<string | undefined> {
|
||||
properties: ['openFile', 'treatPackageAsDirectory'],
|
||||
filters: [
|
||||
{
|
||||
name: 'OS Images',
|
||||
name: i18next.t('source.osImages'),
|
||||
extensions: SUPPORTED_EXTENSIONS,
|
||||
},
|
||||
{
|
||||
name: 'All',
|
||||
name: i18next.t('source.allFiles'),
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
@@ -65,8 +80,8 @@ export async function showWarning(options: {
|
||||
description: string;
|
||||
}): Promise<boolean> {
|
||||
_.defaults(options, {
|
||||
confirmationLabel: 'OK',
|
||||
rejectionLabel: 'Cancel',
|
||||
confirmationLabel: i18next.t('ok'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
});
|
||||
|
||||
const BUTTONS = [options.confirmationLabel, options.rejectionLabel];
|
||||
@@ -84,7 +99,7 @@ export async function showWarning(options: {
|
||||
buttons: BUTTONS,
|
||||
defaultId: BUTTON_REJECTION_INDEX,
|
||||
cancelId: BUTTON_REJECTION_INDEX,
|
||||
title: 'Attention',
|
||||
title: i18next.t('attention'),
|
||||
message: options.title,
|
||||
detail: options.description,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
@@ -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,120 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.page-finish {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.page-finish .title,
|
||||
.page-finish .title h3 {
|
||||
color: $palette-theme-dark-foreground;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.page-finish .center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-finish .box > div > button {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.page-finish webview {
|
||||
width: 800px;
|
||||
height: 300px;
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 0;
|
||||
z-index: 9001;
|
||||
}
|
||||
|
||||
.page-finish .fallback-banner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
color: white;
|
||||
height: 320px;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
|
||||
> * {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.caption {
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.caption-big {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
}
|
||||
|
||||
.caption-small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fallback-footer {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
max-height: 21px;
|
||||
margin-bottom: 17px;
|
||||
}
|
||||
|
||||
.section-footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
.footer-right {
|
||||
color: #7e8085;
|
||||
font-size: 12px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.page-finish .tick {
|
||||
/* hack(Shou): for some reason the height is stretched */
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0 15px 0 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -1,176 +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 { scanner } from 'etcher-sdk';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { TargetSelector } from '../../components/target-selector/target-selector-button';
|
||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
||||
import {
|
||||
isDriveSelected,
|
||||
getImage,
|
||||
getSelectedDrives,
|
||||
deselectDrive,
|
||||
selectDrive,
|
||||
} from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
|
||||
const StepBorder = styled.div<{
|
||||
disabled: boolean;
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
}>`
|
||||
height: 2px;
|
||||
background-color: ${(props) =>
|
||||
props.disabled
|
||||
? props.theme.colors.dark.disabled.foreground
|
||||
: props.theme.colors.dark.foreground};
|
||||
position: absolute;
|
||||
width: 124px;
|
||||
top: 19px;
|
||||
|
||||
left: ${(props) => (props.left ? '-67px' : undefined)};
|
||||
right: ${(props) => (props.right ? '-67px' : undefined)};
|
||||
`;
|
||||
|
||||
const getDriveListLabel = () => {
|
||||
return getSelectedDrives()
|
||||
.map((drive: any) => {
|
||||
return `${drive.description} (${drive.displayName})`;
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const shouldShowDrivesButton = () => {
|
||||
return !settings.getSync('disableExplicitDriveSelection');
|
||||
};
|
||||
|
||||
const getDriveSelectionStateSlice = () => ({
|
||||
showDrivesButton: shouldShowDrivesButton(),
|
||||
driveListLabel: getDriveListLabel(),
|
||||
targets: getSelectedDrives(),
|
||||
image: getImage(),
|
||||
});
|
||||
|
||||
export const selectAllTargets = (
|
||||
modalTargets: scanner.adapters.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 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 [showTargetSelectorModal, setShowTargetSelectorModal] = 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">
|
||||
<DriveSvg className={disabled ? 'disabled' : ''} width="40px" />
|
||||
</div>
|
||||
|
||||
<div className="space-vertical-large">
|
||||
<TargetSelector
|
||||
disabled={disabled}
|
||||
show={!hasDrive && showDrivesButton}
|
||||
tooltip={driveListLabel}
|
||||
openDriveSelector={() => {
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
reselectDrive={() => {
|
||||
analytics.logEvent('Reselect drive');
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
flashing={flashing}
|
||||
targets={targets}
|
||||
image={image}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showTargetSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
cancel={() => setShowTargetSelectorModal(false)}
|
||||
done={(modalTargets) => {
|
||||
selectAllTargets(modalTargets);
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -14,16 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import { Flex, Modal, Txt } from 'rendition';
|
||||
import { Flex, Modal as SmallModal, Txt } from 'rendition';
|
||||
|
||||
import * as constraints from '../../../../shared/drive-constraints';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import { ProgressButton } from '../../components/progress-button/progress-button';
|
||||
import { SourceOptions } from '../../components/source-selector/source-selector';
|
||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
||||
import * as availableDrives from '../../models/available-drives';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
@@ -31,30 +30,18 @@ import * as analytics from '../../modules/analytics';
|
||||
import { scanner as driveScanner } from '../../modules/drive-scanner';
|
||||
import * as imageWriter from '../../modules/image-writer';
|
||||
import * as notification from '../../os/notification';
|
||||
import { selectAllTargets } from './DriveSelector';
|
||||
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';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
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
|
||||
@@ -73,15 +60,35 @@ const getErrorMessageFromCode = (errorCode: string) => {
|
||||
return '';
|
||||
};
|
||||
|
||||
function notifySuccess(
|
||||
iconPath: string,
|
||||
basename: string,
|
||||
drives: any,
|
||||
devices: { successful: number; failed: number },
|
||||
) {
|
||||
notification.send(
|
||||
'Flash complete!',
|
||||
messages.info.flashComplete(basename, drives, devices),
|
||||
iconPath,
|
||||
);
|
||||
}
|
||||
|
||||
function notifyFailure(iconPath: string, basename: string, drives: any) {
|
||||
notification.send(
|
||||
'Oops! Looks like the flash failed.',
|
||||
messages.error.flashFailure(basename, drives),
|
||||
iconPath,
|
||||
);
|
||||
}
|
||||
|
||||
async function flashImageToDrive(
|
||||
isFlashing: boolean,
|
||||
goToSuccess: () => void,
|
||||
sourceOptions: SourceOptions,
|
||||
): Promise<string> {
|
||||
const devices = selection.getSelectedDevices();
|
||||
const image: any = selection.getImage();
|
||||
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
|
||||
return _.includes(devices, drive.device);
|
||||
const drives = availableDrives.getDrives().filter((drive: any) => {
|
||||
return devices.includes(drive.device);
|
||||
});
|
||||
|
||||
if (drives.length === 0 || isFlashing) {
|
||||
@@ -95,26 +102,24 @@ async function flashImageToDrive(
|
||||
const iconPath = path.join('media', 'icon.png');
|
||||
const basename = path.basename(image.path);
|
||||
try {
|
||||
await imageWriter.flash(image.path, drives, sourceOptions);
|
||||
await imageWriter.flash(image, drives);
|
||||
if (!flashState.wasLastFlashCancelled()) {
|
||||
const flashResults: any = flashState.getFlashResults();
|
||||
notification.send(
|
||||
'Flash complete!',
|
||||
messages.info.flashComplete(
|
||||
basename,
|
||||
drives as any,
|
||||
flashResults.results.devices,
|
||||
),
|
||||
iconPath,
|
||||
);
|
||||
const {
|
||||
results = { devices: { successful: 0, failed: 0 } },
|
||||
skip,
|
||||
cancelled,
|
||||
} = flashState.getFlashResults();
|
||||
if (!skip && !cancelled) {
|
||||
if (results.devices.successful > 0) {
|
||||
notifySuccess(iconPath, basename, drives, results.devices);
|
||||
} else {
|
||||
notifyFailure(iconPath, basename, drives);
|
||||
}
|
||||
}
|
||||
goToSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
notification.send(
|
||||
'Oops! Looks like the flash failed.',
|
||||
messages.error.flashFailure(path.basename(image.path), drives),
|
||||
iconPath,
|
||||
);
|
||||
} catch (error: any) {
|
||||
notifyFailure(iconPath, basename, drives);
|
||||
let errorMessage = getErrorMessageFromCode(error.code);
|
||||
if (!errorMessage) {
|
||||
error.image = basename;
|
||||
@@ -131,7 +136,7 @@ async function flashImageToDrive(
|
||||
}
|
||||
|
||||
const formatSeconds = (totalSeconds: number) => {
|
||||
if (!totalSeconds && !_.isNumber(totalSeconds)) {
|
||||
if (typeof totalSeconds !== 'number' || !Number.isFinite(totalSeconds)) {
|
||||
return '';
|
||||
}
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
@@ -143,8 +148,8 @@ const formatSeconds = (totalSeconds: number) => {
|
||||
interface FlashStepProps {
|
||||
shouldFlashStepBeDisabled: boolean;
|
||||
goToSuccess: () => void;
|
||||
source: SourceOptions;
|
||||
isFlashing: boolean;
|
||||
style?: React.CSSProperties;
|
||||
// TODO: factorize
|
||||
step: 'decompressing' | 'flashing' | 'verifying';
|
||||
percentage: number;
|
||||
@@ -152,12 +157,19 @@ interface FlashStepProps {
|
||||
failed: number;
|
||||
speed?: number;
|
||||
eta?: number;
|
||||
width: string;
|
||||
}
|
||||
|
||||
export interface DriveWithWarnings extends constraints.DrivelistDrive {
|
||||
statuses: constraints.DriveStatus[];
|
||||
}
|
||||
|
||||
interface FlashStepState {
|
||||
warningMessages: string[];
|
||||
warningMessage: boolean;
|
||||
errorMessage: string;
|
||||
showDriveSelectorModal: boolean;
|
||||
systemDrives: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}
|
||||
|
||||
export class FlashStep extends React.PureComponent<
|
||||
@@ -167,14 +179,16 @@ export class FlashStep extends React.PureComponent<
|
||||
constructor(props: FlashStepProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
warningMessages: [],
|
||||
warningMessage: false,
|
||||
errorMessage: '',
|
||||
showDriveSelectorModal: false,
|
||||
systemDrives: false,
|
||||
drivesWithWarnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleWarningResponse(shouldContinue: boolean) {
|
||||
this.setState({ warningMessages: [] });
|
||||
this.setState({ warningMessage: false });
|
||||
if (!shouldContinue) {
|
||||
this.setState({ showDriveSelectorModal: true });
|
||||
return;
|
||||
@@ -183,7 +197,6 @@ export class FlashStep extends React.PureComponent<
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.isFlashing,
|
||||
this.props.goToSuccess,
|
||||
this.props.source,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -198,35 +211,49 @@ export class FlashStep extends React.PureComponent<
|
||||
}
|
||||
}
|
||||
|
||||
private hasListWarnings(drives: any[], image: any) {
|
||||
private hasListWarnings(drives: any[]) {
|
||||
if (drives.length === 0 || flashState.isFlashing()) {
|
||||
return;
|
||||
}
|
||||
return constraints.hasListDriveImageCompatibilityStatus(drives, image);
|
||||
return drives.filter((drive) => drive.isSystem).length > 0;
|
||||
}
|
||||
|
||||
private async tryFlash() {
|
||||
const devices = selection.getSelectedDevices();
|
||||
const image = selection.getImage();
|
||||
const drives = _.filter(
|
||||
availableDrives.getDrives(),
|
||||
(drive: { device: string }) => {
|
||||
return _.includes(devices, drive.device);
|
||||
},
|
||||
);
|
||||
const drives = selection.getSelectedDrives().map((drive) => {
|
||||
return {
|
||||
...drive,
|
||||
statuses: constraints.getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
};
|
||||
});
|
||||
if (drives.length === 0 || this.props.isFlashing) {
|
||||
return;
|
||||
}
|
||||
const hasDangerStatus = this.hasListWarnings(drives, image);
|
||||
const hasDangerStatus = drives.some((drive) => drive.statuses.length > 0);
|
||||
if (hasDangerStatus) {
|
||||
this.setState({ warningMessages: getWarningMessages(drives, image) });
|
||||
const systemDrives = drives.some((drive) =>
|
||||
drive.statuses.includes(constraints.statuses.system),
|
||||
);
|
||||
this.setState({
|
||||
systemDrives,
|
||||
drivesWithWarnings: drives.filter((driveWithWarnings) => {
|
||||
return (
|
||||
driveWithWarnings.isSystem ||
|
||||
(!systemDrives &&
|
||||
driveWithWarnings.statuses.includes(constraints.statuses.large))
|
||||
);
|
||||
}),
|
||||
warningMessage: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.isFlashing,
|
||||
this.props.goToSuccess,
|
||||
this.props.source,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -234,85 +261,74 @@ export class FlashStep extends React.PureComponent<
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="box text-center">
|
||||
<div className="center-block">
|
||||
<FlashSvg
|
||||
width="40px"
|
||||
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-vertical-large">
|
||||
<ProgressButton
|
||||
type={this.props.step}
|
||||
active={this.props.isFlashing}
|
||||
percentage={this.props.percentage}
|
||||
position={this.props.position}
|
||||
disabled={this.props.shouldFlashStepBeDisabled}
|
||||
cancel={imageWriter.cancel}
|
||||
warning={this.hasListWarnings(
|
||||
selection.getSelectedDrives(),
|
||||
selection.getImage(),
|
||||
)}
|
||||
callback={() => {
|
||||
this.tryFlash();
|
||||
}}
|
||||
/>
|
||||
|
||||
{!_.isNil(this.props.speed) &&
|
||||
this.props.percentage !== COMPLETED_PERCENTAGE && (
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
fontSize="14px"
|
||||
color="#7e8085"
|
||||
>
|
||||
{!_.isNil(this.props.speed) && (
|
||||
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
|
||||
)}
|
||||
{!_.isNil(this.props.eta) && (
|
||||
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{Boolean(this.props.failed) && (
|
||||
<div className="target-status-wrap">
|
||||
<div className="target-status-line target-status-failed">
|
||||
<span className="target-status-dot"></span>
|
||||
<span className="target-status-quantity">
|
||||
{this.props.failed}
|
||||
</span>
|
||||
<span className="target-status-message">
|
||||
{messages.progress.failed(this.props.failed)}{' '}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.warningMessages.length > 0 && (
|
||||
<Modal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => this.handleWarningResponse(false)}
|
||||
done={() => this.handleWarningResponse(true)}
|
||||
cancelButtonProps={{
|
||||
children: 'Change',
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="start"
|
||||
width={this.props.width}
|
||||
style={this.props.style}
|
||||
>
|
||||
<FlashSvg
|
||||
width="40px"
|
||||
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
|
||||
style={{
|
||||
margin: '0 auto',
|
||||
}}
|
||||
action={'Continue'}
|
||||
primaryButtonProps={{ primary: false, warning: true }}
|
||||
>
|
||||
{_.map(this.state.warningMessages, (message, key) => (
|
||||
<Txt key={key} whitespace="pre-line" mt={2}>
|
||||
{message}
|
||||
</Txt>
|
||||
))}
|
||||
</Modal>
|
||||
/>
|
||||
|
||||
<ProgressButton
|
||||
type={this.props.step}
|
||||
active={this.props.isFlashing}
|
||||
percentage={this.props.percentage}
|
||||
position={this.props.position}
|
||||
disabled={this.props.shouldFlashStepBeDisabled}
|
||||
cancel={imageWriter.cancel}
|
||||
warning={this.hasListWarnings(selection.getSelectedDrives())}
|
||||
callback={() => this.tryFlash()}
|
||||
/>
|
||||
|
||||
{!_.isNil(this.props.speed) &&
|
||||
this.props.percentage !== COMPLETED_PERCENTAGE && (
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
fontSize="14px"
|
||||
color="#7e8085"
|
||||
width="100%"
|
||||
>
|
||||
<Txt>
|
||||
{i18next.t('flash.speedShort', {
|
||||
speed: this.props.speed.toFixed(SPEED_PRECISION),
|
||||
})}
|
||||
</Txt>
|
||||
{!_.isNil(this.props.eta) && (
|
||||
<Txt>
|
||||
{i18next.t('flash.eta', {
|
||||
eta: formatSeconds(this.props.eta),
|
||||
})}
|
||||
</Txt>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{Boolean(this.props.failed) && (
|
||||
<Flex color="#fff" alignItems="center" mt={35}>
|
||||
<CircleSvg height="1em" fill="#ff4444" />
|
||||
<Txt ml={10}>{this.props.failed}</Txt>
|
||||
<Txt ml={10}>{messages.progress.failed(this.props.failed)}</Txt>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{this.state.warningMessage && (
|
||||
<DriveStatusWarningModal
|
||||
done={() => this.handleWarningResponse(true)}
|
||||
cancel={() => this.handleWarningResponse(false)}
|
||||
isSystem={this.state.systemDrives}
|
||||
drivesWithWarnings={this.state.drivesWithWarnings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.errorMessage && (
|
||||
<Modal
|
||||
<SmallModal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => this.handleFlashErrorResponse(false)}
|
||||
@@ -320,20 +336,21 @@ export class FlashStep extends React.PureComponent<
|
||||
action={'Retry'}
|
||||
>
|
||||
<Txt>
|
||||
{_.map(this.state.errorMessage.split('\n'), (message, key) => (
|
||||
{this.state.errorMessage.split('\n').map((message, key) => (
|
||||
<p key={key}>{message}</p>
|
||||
))}
|
||||
</Txt>
|
||||
</Modal>
|
||||
</SmallModal>
|
||||
)}
|
||||
{this.state.showDriveSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
write={true}
|
||||
cancel={() => this.setState({ showDriveSelectorModal: false })}
|
||||
done={(modalTargets) => {
|
||||
selectAllTargets(modalTargets);
|
||||
this.setState({ showDriveSelectorModal: false });
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -14,22 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { faCog, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
|
||||
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
||||
|
||||
import * as path from 'path';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { FeaturedProject } from '../../components/featured-project/featured-project';
|
||||
import FinishPage from '../../components/finish/finish';
|
||||
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 {
|
||||
SourceOptions,
|
||||
SourceMetadata,
|
||||
SourceSelector,
|
||||
} from '../../components/source-selector/source-selector';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
@@ -41,14 +39,15 @@ import {
|
||||
IconButton as BaseIcon,
|
||||
ThemedProvider,
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
|
||||
import { DriveSelector } from './DriveSelector';
|
||||
import {
|
||||
TargetSelector,
|
||||
getDriveListLabel,
|
||||
} from '../../components/target-selector/target-selector';
|
||||
import { FlashStep } from './Flash';
|
||||
|
||||
import EtcherSvg from '../../../assets/etcher.svg';
|
||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||
|
||||
const Icon = styled(BaseIcon)`
|
||||
margin-right: 20px;
|
||||
@@ -68,31 +67,54 @@ function getDrivesTitle() {
|
||||
return `${drives.length} Targets`;
|
||||
}
|
||||
|
||||
function getImageBasename() {
|
||||
if (!selectionState.hasImage()) {
|
||||
function getImageBasename(image?: SourceMetadata) {
|
||||
if (image === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const selectionImageName = selectionState.getImageName();
|
||||
const imageBasename = path.basename(selectionState.getImagePath());
|
||||
return selectionImageName || imageBasename;
|
||||
if (image.drive) {
|
||||
return image.drive.description;
|
||||
}
|
||||
const imageBasename = path.basename(image.path);
|
||||
return image.name || imageBasename;
|
||||
}
|
||||
|
||||
const StepBorder = styled.div<{
|
||||
disabled: boolean;
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
height: 2px;
|
||||
background-color: ${(props) =>
|
||||
props.disabled
|
||||
? props.theme.colors.dark.disabled.foreground
|
||||
: props.theme.colors.dark.foreground};
|
||||
width: 120px;
|
||||
top: 19px;
|
||||
|
||||
left: ${(props) => (props.left ? '-67px' : undefined)};
|
||||
margin-right: ${(props) => (props.left ? '-120px' : undefined)};
|
||||
right: ${(props) => (props.right ? '-67px' : undefined)};
|
||||
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
|
||||
`;
|
||||
|
||||
interface MainPageStateFromStore {
|
||||
isFlashing: boolean;
|
||||
hasImage: boolean;
|
||||
hasDrive: boolean;
|
||||
imageLogo: string;
|
||||
imageSize: number;
|
||||
imageName: string;
|
||||
imageLogo?: string;
|
||||
imageSize?: number;
|
||||
imageName?: string;
|
||||
driveTitle: string;
|
||||
driveLabel: string;
|
||||
}
|
||||
|
||||
interface MainPageState {
|
||||
current: 'main' | 'success';
|
||||
isWebviewShowing: boolean;
|
||||
hideSettings: boolean;
|
||||
source: SourceOptions;
|
||||
featuredProjectURL?: string;
|
||||
}
|
||||
|
||||
export class MainPage extends React.Component<
|
||||
@@ -105,30 +127,39 @@ export class MainPage extends React.Component<
|
||||
current: 'main',
|
||||
isWebviewShowing: false,
|
||||
hideSettings: true,
|
||||
source: {
|
||||
imagePath: '',
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
...this.stateHelper(),
|
||||
};
|
||||
}
|
||||
|
||||
private stateHelper(): MainPageStateFromStore {
|
||||
const image = selectionState.getImage();
|
||||
return {
|
||||
isFlashing: flashState.isFlashing(),
|
||||
hasImage: selectionState.hasImage(),
|
||||
hasDrive: selectionState.hasDrive(),
|
||||
imageLogo: selectionState.getImageLogo(),
|
||||
imageSize: selectionState.getImageSize(),
|
||||
imageName: getImageBasename(),
|
||||
imageLogo: image?.logo,
|
||||
imageSize: image?.size,
|
||||
imageName: getImageBasename(selectionState.getImage()),
|
||||
driveTitle: getDrivesTitle(),
|
||||
driveLabel: getDriveListLabel(),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
private async getFeaturedProjectURL() {
|
||||
const url = new URL(
|
||||
(await settings.get('featuredProjectEndpoint')) ||
|
||||
'https://assets.balena.io/etcher-featured/index.html',
|
||||
);
|
||||
url.searchParams.append('borderRight', 'false');
|
||||
url.searchParams.append('darkBackground', 'true');
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
observe(() => {
|
||||
this.setState(this.stateHelper());
|
||||
});
|
||||
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
|
||||
}
|
||||
|
||||
private renderMain() {
|
||||
@@ -136,55 +167,164 @@ export class MainPage extends React.Component<
|
||||
const shouldDriveStepBeDisabled = !this.state.hasImage;
|
||||
const shouldFlashStepBeDisabled =
|
||||
!this.state.hasImage || !this.state.hasDrive;
|
||||
const notFlashingOrSplitView =
|
||||
!this.state.isFlashing || !this.state.isWebviewShowing;
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
id="app-header"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '13px 14px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() =>
|
||||
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
|
||||
}
|
||||
tabIndex={100}
|
||||
>
|
||||
<EtcherSvg width="123px" height="22px" />
|
||||
</span>
|
||||
<Flex
|
||||
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{notFlashingOrSplitView && (
|
||||
<>
|
||||
<SourceSelector flashing={this.state.isFlashing} />
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||
</Flex>
|
||||
<TargetSelector
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
/>
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
<span
|
||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
||||
<Flex
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '36.2vw',
|
||||
height: '100vh',
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<ReducedFlashingInfos
|
||||
imageLogo={this.state.imageLogo}
|
||||
imageName={this.state.imageName}
|
||||
imageSize={
|
||||
typeof this.state.imageSize === 'number'
|
||||
? prettyBytes(this.state.imageSize)
|
||||
: ''
|
||||
}
|
||||
driveTitle={this.state.driveTitle}
|
||||
driveLabel={this.state.driveLabel}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
color: '#fff',
|
||||
left: 35,
|
||||
top: 72,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{this.state.isFlashing && this.state.featuredProjectURL && (
|
||||
<SafeWebview
|
||||
src={this.state.featuredProjectURL}
|
||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||
this.setState({ isWebviewShowing });
|
||||
}}
|
||||
style={{
|
||||
float: 'right',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
/>
|
||||
)}
|
||||
|
||||
<FlashStep
|
||||
width={this.state.isWebviewShowing ? '220px' : '200px'}
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
isFlashing={this.state.isFlashing}
|
||||
step={state.type}
|
||||
percentage={state.percentage}
|
||||
position={state.position}
|
||||
failed={state.failed}
|
||||
speed={state.speed}
|
||||
eta={state.eta}
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSuccess() {
|
||||
return (
|
||||
<FinishPage
|
||||
goToMain={() => {
|
||||
flashState.resetState();
|
||||
this.setState({ current: 'main' });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<ThemedProvider style={{ height: '100%', width: '100%' }}>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
paddingTop="14px"
|
||||
style={{
|
||||
// Allow window to be dragged from header
|
||||
// @ts-ignore
|
||||
WebkitAppRegion: 'drag',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<Flex width="100%" />
|
||||
<Flex width="100%" alignItems="center" justifyContent="center">
|
||||
<EtcherSvg
|
||||
width="123px"
|
||||
height="22px"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() =>
|
||||
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
|
||||
}
|
||||
tabIndex={100}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex width="100%" alignItems="center" justifyContent="flex-end">
|
||||
<Icon
|
||||
icon={<FontAwesomeIcon icon={faCog} />}
|
||||
icon={<CogSvg height="1em" fill="currentColor" />}
|
||||
plain
|
||||
tabIndex={5}
|
||||
onClick={() => this.setState({ hideSettings: false })}
|
||||
style={{
|
||||
// Make touch events click instead of dragging
|
||||
WebkitAppRegion: 'no-drag',
|
||||
}}
|
||||
/>
|
||||
{!settings.getSync('disableExternalLinks') && (
|
||||
<Icon
|
||||
icon={<FontAwesomeIcon icon={faQuestionCircle} />}
|
||||
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
selectionState.getImageSupportUrl() ||
|
||||
selectionState.getImage()?.supportUrl ||
|
||||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
|
||||
)
|
||||
}
|
||||
tabIndex={6}
|
||||
style={{
|
||||
// Make touch events click instead of dragging
|
||||
WebkitAppRegion: 'no-drag',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</header>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{this.state.hideSettings ? null : (
|
||||
<SettingsModal
|
||||
toggleModal={(value: boolean) => {
|
||||
@@ -192,96 +332,6 @@ export class MainPage extends React.Component<
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
className="page-main row around-xs"
|
||||
style={{ margin: '110px 50px' }}
|
||||
>
|
||||
<div className="col-xs">
|
||||
<SourceSelector
|
||||
flashing={this.state.isFlashing}
|
||||
afterSelected={(source: SourceOptions) =>
|
||||
this.setState({ source })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-xs">
|
||||
<DriveSelector
|
||||
webviewShowing={this.state.isWebviewShowing}
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
nextStepDisabled={shouldFlashStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{this.state.isFlashing && (
|
||||
<div
|
||||
className={`featured-project ${
|
||||
this.state.isFlashing && this.state.isWebviewShowing
|
||||
? 'fp-visible'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<FeaturedProject
|
||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||
this.setState({ isWebviewShowing });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<ReducedFlashingInfos
|
||||
imageLogo={this.state.imageLogo}
|
||||
imageName={middleEllipsis(this.state.imageName, 16)}
|
||||
imageSize={
|
||||
_.isNumber(this.state.imageSize)
|
||||
? (bytesToClosestUnit(this.state.imageSize) as string)
|
||||
: ''
|
||||
}
|
||||
driveTitle={middleEllipsis(this.state.driveTitle, 16)}
|
||||
shouldShow={this.state.isFlashing && this.state.isWebviewShowing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-xs">
|
||||
<FlashStep
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
source={this.state.source}
|
||||
isFlashing={flashState.isFlashing()}
|
||||
step={state.type}
|
||||
percentage={state.percentage}
|
||||
position={state.position}
|
||||
failed={state.failed}
|
||||
speed={state.speed}
|
||||
eta={state.eta}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSuccess() {
|
||||
return (
|
||||
<div className="section-loader isFinish">
|
||||
<FinishPage
|
||||
goToMain={() => {
|
||||
flashState.resetState();
|
||||
this.setState({ current: 'main' });
|
||||
}}
|
||||
/>
|
||||
<SafeWebview src="https://www.balena.io/etcher/success-banner/" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<ThemedProvider style={{ height: '100%', width: '100%' }}>
|
||||
{this.state.current === 'main'
|
||||
? this.renderMain()
|
||||
: this.renderSuccess()}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.disabled {
|
||||
opacity: $disabled-opacity;
|
||||
}
|
||||
|
||||
.page-main {
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.page-main > .col-xs {
|
||||
height: 165px;
|
||||
}
|
||||
|
||||
.page-main .relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-main .glyphicon {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.page-main .step-name {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 39px;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
color: $palette-theme-primary-foreground;
|
||||
}
|
||||
|
||||
.target-status-wrap {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 62px;
|
||||
flex-direction: column;
|
||||
margin: 8px 28px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.target-status-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
> .target-status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.target-status-successful > .target-status-dot {
|
||||
background-color: $palette-theme-success-background;
|
||||
}
|
||||
&.target-status-failed > .target-status-dot {
|
||||
background-color: $palette-theme-danger-background;
|
||||
}
|
||||
|
||||
> .target-status-quantity {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .target-status-message {
|
||||
color: gray;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.space-vertical-large {
|
||||
position: relative;
|
||||
}
|
||||
15
lib/gui/app/renderer.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// @ts-nocheck
|
||||
import { main } from './app';
|
||||
import './i18n';
|
||||
import { langParser } from './i18n';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
ipcRenderer.send('change-lng', langParser());
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./app', () => {
|
||||
main();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
$icon-font-path: "../../../../node_modules/bootstrap-sass/assets/fonts/bootstrap/";
|
||||
$font-size-base: 16px;
|
||||
$cursor-disabled: initial;
|
||||
$link-hover-decoration: none;
|
||||
$btn-min-width: 170px;
|
||||
$link-color: #ddd;
|
||||
$disabled-opacity: 0.2;
|
||||
|
||||
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
|
||||
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
|
||||
@import "./modules/theme";
|
||||
@import "./modules/space";
|
||||
@import "../pages/main/styles/main";
|
||||
@import "../pages/finish/styles/finish";
|
||||
@import "./desktop";
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
// Prevent white flash when running application
|
||||
html {
|
||||
background-color: $palette-theme-dark-background;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: $palette-theme-dark-background;
|
||||
letter-spacing: 0.1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: "SourceSansPro";
|
||||
|
||||
> header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
> main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
> footer {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.section-loader {
|
||||
webview {
|
||||
flex: 0 1;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&.isFinish webview {
|
||||
flex: initial;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.featured-project {
|
||||
webview {
|
||||
flex: 0 1;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&.fp-visible webview {
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 30px;
|
||||
top: 45px;
|
||||
border-radius: 7px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -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,16 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert as AlertBase,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Button,
|
||||
ButtonProps,
|
||||
Modal as ModalBase,
|
||||
Provider,
|
||||
Table as BaseTable,
|
||||
TableProps as BaseTableProps,
|
||||
Txt,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
import { space } from 'styled-system';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { colors, theme } from './theme';
|
||||
|
||||
@@ -54,7 +59,7 @@ export const StepButton = styled((props: ButtonProps) => (
|
||||
<BaseButton {...props}></BaseButton>
|
||||
))`
|
||||
color: #ffffff;
|
||||
margin: auto;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const ChangeButton = styled(Button)`
|
||||
@@ -72,7 +77,6 @@ export const ChangeButton = styled(Button)`
|
||||
color: #8f9297;
|
||||
}
|
||||
}
|
||||
${space}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -81,7 +85,7 @@ export const StepNameButton = styled(BaseButton)`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
color: ${colors.dark.foreground};
|
||||
|
||||
&:enabled {
|
||||
@@ -99,46 +103,223 @@ export const Footer = styled(Txt)`
|
||||
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((props) => {
|
||||
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.length ? children.map((c: any) => <>{c}</>) : children}
|
||||
</ScrollableFlex>
|
||||
</ModalBase>
|
||||
);
|
||||
})`
|
||||
> div {
|
||||
padding: 30px;
|
||||
height: calc(100% - 80px);
|
||||
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;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-shadow: 0 -2px 10px 0 rgba(221, 225, 240, 0.5), 0 -1px 0 0 #dde1f0;
|
||||
}
|
||||
|
||||
::-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',
|
||||
@@ -65,8 +68,25 @@ export const colors = {
|
||||
},
|
||||
};
|
||||
|
||||
export const theme = {
|
||||
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',
|
||||
@@ -76,21 +96,31 @@ export const theme = {
|
||||
opacity: 1,
|
||||
},
|
||||
extend: () => `
|
||||
width: 200px;
|
||||
font-size: 16px;
|
||||
|
||||
&& {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
: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};
|
||||
opacity: 1;
|
||||
|
||||
:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
};
|
||||
layer: {
|
||||
extend: () => `
|
||||
> div:first-child {
|
||||
background-color: transparent;
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
73
lib/gui/app/utils/etcher-pro-specific.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Dictionary } from 'lodash';
|
||||
|
||||
type BalenaTag = {
|
||||
id: number,
|
||||
name: string,
|
||||
value: string
|
||||
}
|
||||
|
||||
export class EtcherPro {
|
||||
private supervisorAddr: string;
|
||||
private supervisorKey: string;
|
||||
private tags: Dictionary<string> | undefined;
|
||||
public uuid: string;
|
||||
|
||||
constructor(supervisorAddr: string, supervisorKey: string) {
|
||||
this.supervisorAddr = supervisorAddr;
|
||||
this.supervisorKey = supervisorKey;
|
||||
this.uuid = (process.env.BALENA_DEVICE_UUID ?? 'NO-UUID').substring(0, 7);
|
||||
this.tags = undefined;
|
||||
this.get_tags().then((tags) => (this.tags = tags));
|
||||
}
|
||||
|
||||
async get_tags(): Promise<Dictionary<string>> {
|
||||
const result = await fetch(
|
||||
this.supervisorAddr + '/v2/device/tags?apikey=' + this.supervisorKey,
|
||||
);
|
||||
const parsed = await result.json();
|
||||
if (parsed['status'] === 'success') {
|
||||
return Object.assign(
|
||||
{},
|
||||
...parsed['tags'].map((tag: BalenaTag) => {
|
||||
return { [tag.name]: tag.value };
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
public get_serial(): string | undefined {
|
||||
if (this.tags) {
|
||||
return this.tags['Serial'];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function etcherProInfo(): EtcherPro | undefined {
|
||||
const BALENA_SUPERVISOR_ADDRESS = process.env.BALENA_SUPERVISOR_ADDRESS;
|
||||
const BALENA_SUPERVISOR_API_KEY = process.env.BALENA_SUPERVISOR_API_KEY;
|
||||
|
||||
if (BALENA_SUPERVISOR_ADDRESS && BALENA_SUPERVISOR_API_KEY) {
|
||||
return new EtcherPro(BALENA_SUPERVISOR_ADDRESS, BALENA_SUPERVISOR_API_KEY);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -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,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,12 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="39" height="90" viewBox="0 0 39 90">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M30.88 39.87H7.517v23.21c0 .69.561 1.25 1.251 1.25H29.63c.692 0 1.251-.56 1.251-1.25V39.87zm-22.363 1H29.88v22.21c0 .138-.112.25-.25.25H8.767l-.057-.007c-.11-.026-.194-.125-.194-.244V40.87z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M16.558 48.925H12.59c-.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.054H12.59c-.03 0-.055-.024-.055-.054v-2.733c0-.03.024-.054.055-.054zM25.97 48.925h-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.967c-.03 0-.055-.024-.055-.054v-2.733c0-.03.024-.054.055-.054z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" d="M37.398 35.952c0 2.43-1.988 4.418-4.418 4.418H5.418C2.988 40.37 1 38.382 1 35.952V5.418C1 2.988 2.988 1 5.418 1H32.98c2.43 0 4.418 1.988 4.418 4.418v30.534z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M32.98 0H5.418C2.436 0 0 2.436 0 5.418v30.534c0 2.982 2.436 5.418 5.418 5.418H32.98c2.982 0 5.418-2.436 5.418-5.418V5.418C38.398 2.436 35.962 0 32.98 0zM5.418 2H32.98c1.878 0 3.418 1.54 3.418 3.418v30.534c0 1.878-1.54 3.418-3.418 3.418H5.418C3.54 39.37 2 37.83 2 35.952V5.418C2 3.54 3.54 2 5.418 2z" transform="translate(.5)"/>
|
||||
<path fill="#FFF" fill-rule="nonzero" d="M13.567 25v-8.634h2.918v-1.031H9.413v1.031h2.917V25h1.237zm5.869 3.3c.56 0 1.063-.066 1.51-.199.447-.132.828-.311 1.142-.537.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.17c-.491 0-.825-.074-1.002-.221-.177-.147-.265-.334-.265-.56 0-.196.044-.36.132-.493.089-.133.197-.253.324-.361.167.078.344.14.53.184.187.044.37.066.546.066.363 0 .705-.059 1.024-.177.32-.118.597-.282.832-.493.236-.211.423-.472.56-.781.138-.31.207-.656.207-1.039 0-.304-.057-.584-.17-.84-.113-.255-.253-.466-.42-.633h1.474v-.928h-2.49c-.138-.05-.293-.091-.465-.126-.171-.034-.356-.051-.552-.051-.363 0-.71.059-1.039.177-.329.117-.616.287-.862.508-.245.22-.44.489-.582.803-.142.314-.213.668-.213 1.06 0 .433.096.813.287 1.142.192.33.405.592.641.789v.059c-.187.127-.363.304-.53.53-.167.226-.25.491-.25.796 0 .285.06.523.183.714.123.192.273.342.45.45v.059c-.324.225-.58.476-.766.75-.187.276-.28.566-.28.87 0 .315.07.59.213.825.143.236.344.437.604.604.26.167.572.293.936.376.363.084.766.125 1.208.125zm0-6.38c-.206 0-.4-.039-.582-.117-.182-.079-.344-.192-.486-.339-.143-.147-.253-.327-.332-.538-.078-.211-.118-.45-.118-.714 0-.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.715-.079.211-.19.39-.332.538-.142.147-.304.26-.486.339-.182.078-.376.118-.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.133-.147.039-.285.059-.413.059-.412 0-.7-.12-.861-.361-.162-.241-.244-.582-.244-1.024v-3.978h1.93v-.987h-1.93v-2.004h-1.016L24.7 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" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M19.147 73.55c.245 0 .45.178.492.41l.008.09v14.883c0 .276-.224.5-.5.5-.245 0-.45-.177-.492-.41l-.008-.09V74.05c0-.276.224-.5.5-.5z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M14.182 83.856c.176-.171.446-.188.639-.05l.068.058 4.615 4.719c.194.197.19.514-.007.707-.176.172-.446.188-.639.05l-.068-.058-4.615-4.719c-.194-.197-.19-.514.007-.707z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M23.516 83.96c.198-.193.514-.19.707.008.172.175.188.445.051.638l-.058.07-4.72 4.614c-.197.193-.513.19-.706-.008-.172-.175-.188-.445-.051-.638l.058-.069 4.72-4.615z" transform="translate(.5)"/>
|
||||
</g>
|
||||
</svg>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -14,27 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { delay } from 'bluebird';
|
||||
import * as electron from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { promises as fs } from 'fs';
|
||||
import { platform } from 'os';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import './app/i18n';
|
||||
|
||||
import { packageType, version } from '../../package.json';
|
||||
import * as EXIT_CODES from '../shared/exit-codes';
|
||||
import { getConfig } from '../shared/utils';
|
||||
import { delay, getConfig } from '../shared/utils';
|
||||
import * as settings from './app/models/settings';
|
||||
import * as analytics from './app/modules/analytics';
|
||||
import { logException } from './app/modules/analytics';
|
||||
import { buildWindowMenu } from './menu';
|
||||
import * as i18n from 'i18next';
|
||||
|
||||
const customProtocol = 'etcher';
|
||||
const scheme = `${customProtocol}://`;
|
||||
const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
|
||||
const packageUpdatable = _.includes(updatablePackageTypes, packageType);
|
||||
const packageUpdatable = updatablePackageTypes.includes(packageType);
|
||||
let packageUpdated = false;
|
||||
let mainWindow: any = null;
|
||||
|
||||
async function checkForUpdates(interval: number) {
|
||||
// We use a while loop instead of a setInterval to preserve
|
||||
@@ -45,13 +47,13 @@ async function checkForUpdates(interval: number) {
|
||||
const release = await autoUpdater.checkForUpdates();
|
||||
const isOutdated =
|
||||
semver.compare(release.updateInfo.version, version) > 0;
|
||||
const shouldUpdate = release.updateInfo.stagingPercentage || 0 > 0;
|
||||
const shouldUpdate = release.updateInfo.stagingPercentage !== 0; // undefinded (default) means 100%
|
||||
if (shouldUpdate && isOutdated) {
|
||||
await autoUpdater.downloadUpdate();
|
||||
packageUpdated = true;
|
||||
}
|
||||
} catch (err) {
|
||||
analytics.logException(err);
|
||||
logException(err);
|
||||
}
|
||||
}
|
||||
await delay(interval);
|
||||
@@ -99,6 +101,7 @@ const sourceSelectorReady = new Promise((resolve) => {
|
||||
async function selectImageURL(url?: string) {
|
||||
// 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests
|
||||
if (url !== undefined && url !== 'data:,') {
|
||||
url = url.replace(/\/$/, ''); // on windows the url ends with an extra slash
|
||||
url = url.startsWith(scheme) ? url.slice(scheme.length) : url;
|
||||
await sourceSelectorReady;
|
||||
electron.BrowserWindow.getAllWindows().forEach((window) => {
|
||||
@@ -114,20 +117,28 @@ electron.app.on('open-url', async (event, data) => {
|
||||
await selectImageURL(data);
|
||||
});
|
||||
|
||||
interface AutoUpdaterConfig {
|
||||
autoDownload?: boolean;
|
||||
autoInstallOnAppQuit?: boolean;
|
||||
allowPrerelease?: boolean;
|
||||
fullChangelog?: boolean;
|
||||
allowDowngrade?: boolean;
|
||||
}
|
||||
|
||||
async function createMainWindow() {
|
||||
const fullscreen = Boolean(await settings.get('fullscreen'));
|
||||
const defaultWidth = 800;
|
||||
const defaultHeight = 480;
|
||||
const defaultWidth = settings.DEFAULT_WIDTH;
|
||||
const defaultHeight = settings.DEFAULT_HEIGHT;
|
||||
let width = defaultWidth;
|
||||
let height = defaultHeight;
|
||||
if (fullscreen) {
|
||||
({ width, height } = electron.screen.getPrimaryDisplay().bounds);
|
||||
}
|
||||
const mainWindow = new electron.BrowserWindow({
|
||||
mainWindow = new electron.BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
frame: !fullscreen,
|
||||
useContentSize: false,
|
||||
useContentSize: true,
|
||||
show: false,
|
||||
resizable: false,
|
||||
maximizable: false,
|
||||
@@ -141,6 +152,7 @@ async function createMainWindow() {
|
||||
webPreferences: {
|
||||
backgroundThrottling: false,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
webviewTag: true,
|
||||
zoomFactor: width / defaultWidth,
|
||||
enableRemoteModule: true,
|
||||
@@ -149,12 +161,14 @@ async function createMainWindow() {
|
||||
|
||||
electron.app.setAsDefaultProtocolClient(customProtocol);
|
||||
|
||||
buildWindowMenu(mainWindow);
|
||||
mainWindow.setFullScreen(true);
|
||||
|
||||
// Prevent flash of white when starting the application
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
console.timeEnd('ready-to-show');
|
||||
// Electron sometimes caches the zoomFactor
|
||||
// making it obnoxious to switch back-and-forth
|
||||
mainWindow.webContents.setZoomFactor(width / defaultWidth);
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
@@ -165,33 +179,36 @@ async function createMainWindow() {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
mainWindow.loadURL(`file://${path.join(__dirname, 'index.html')}`);
|
||||
mainWindow.loadURL(
|
||||
`file://${path.join(
|
||||
'/',
|
||||
...__dirname.split(path.sep).map(encodeURIComponent),
|
||||
'index.html',
|
||||
)}`,
|
||||
);
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
page.once('did-frame-finish-load', async () => {
|
||||
autoUpdater.on('error', (err) => {
|
||||
analytics.logException(err);
|
||||
logException(err);
|
||||
});
|
||||
if (packageUpdatable) {
|
||||
try {
|
||||
const onlineConfig = await getConfig();
|
||||
const autoUpdaterConfig = _.get(
|
||||
onlineConfig,
|
||||
['autoUpdates', 'autoUpdaterConfig'],
|
||||
{
|
||||
autoDownload: false,
|
||||
},
|
||||
);
|
||||
_.merge(autoUpdater, autoUpdaterConfig);
|
||||
const checkForUpdatesTimer = _.get(
|
||||
onlineConfig,
|
||||
['autoUpdates', 'checkForUpdatesTimer'],
|
||||
300000,
|
||||
);
|
||||
const configUrl = await settings.get('configUrl');
|
||||
const onlineConfig = await getConfig(configUrl);
|
||||
const autoUpdaterConfig: AutoUpdaterConfig = onlineConfig?.autoUpdates
|
||||
?.autoUpdaterConfig ?? {
|
||||
autoDownload: false,
|
||||
};
|
||||
for (const [key, value] of Object.entries(autoUpdaterConfig)) {
|
||||
autoUpdater[key as keyof AutoUpdaterConfig] = value;
|
||||
}
|
||||
const checkForUpdatesTimer =
|
||||
onlineConfig?.autoUpdates?.checkForUpdatesTimer ?? 300000;
|
||||
checkForUpdates(checkForUpdatesTimer);
|
||||
} catch (err) {
|
||||
analytics.logException(err);
|
||||
logException(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -226,6 +243,17 @@ async function main(): Promise<void> {
|
||||
await selectImageURL(await getCommandLineURL(argv));
|
||||
});
|
||||
await selectImageURL(await getCommandLineURL(process.argv));
|
||||
|
||||
electron.ipcMain.on('change-lng', function (event, args) {
|
||||
i18n.changeLanguage(args, () => {
|
||||
console.log('Language changed to: ' + args);
|
||||
});
|
||||
if (mainWindow != null) {
|
||||
buildWindowMenu(mainWindow);
|
||||
} else {
|
||||
console.log('Build menu failed. ');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
import * as electron from 'electron';
|
||||
import { displayName } from '../../package.json';
|
||||
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
/**
|
||||
* @summary Builds a native application menu for a given window
|
||||
*/
|
||||
@@ -42,12 +44,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
const menuTemplate: electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
role: 'editMenu',
|
||||
label: i18next.t('menu.edit'),
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
label: i18next.t('menu.view'),
|
||||
submenu: [
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
label: i18next.t('menu.devTool'),
|
||||
accelerator:
|
||||
process.platform === 'darwin' ? 'Command+Alt+I' : 'Control+Shift+I',
|
||||
click: toggleDevTools,
|
||||
@@ -56,12 +59,14 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
},
|
||||
{
|
||||
role: 'windowMenu',
|
||||
label: i18next.t('menu.window'),
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
label: i18next.t('menu.help'),
|
||||
submenu: [
|
||||
{
|
||||
label: 'Etcher Pro',
|
||||
label: i18next.t('menu.pro'),
|
||||
click() {
|
||||
electron.shell.openExternal(
|
||||
'https://etcher.io/pro?utm_source=etcher_menu&ref=etcher_menu',
|
||||
@@ -69,13 +74,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Etcher Website',
|
||||
label: i18next.t('menu.website'),
|
||||
click() {
|
||||
electron.shell.openExternal('https://etcher.io?ref=etcher_menu');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Report an issue',
|
||||
label: i18next.t('menu.issue'),
|
||||
click() {
|
||||
electron.shell.openExternal(
|
||||
'https://github.com/balena-io/etcher/issues',
|
||||
@@ -92,25 +97,29 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
submenu: [
|
||||
{
|
||||
role: 'about' as const,
|
||||
label: 'About Etcher',
|
||||
label: i18next.t('menu.about'),
|
||||
},
|
||||
{
|
||||
type: 'separator' as const,
|
||||
},
|
||||
{
|
||||
role: 'hide' as const,
|
||||
label: i18next.t('menu.hide'),
|
||||
},
|
||||
{
|
||||
role: 'hideOthers' as const,
|
||||
label: i18next.t('menu.hideOthers'),
|
||||
},
|
||||
{
|
||||
role: 'unhide' as const,
|
||||
label: i18next.t('menu.unhide'),
|
||||
},
|
||||
{
|
||||
type: 'separator' as const,
|
||||
},
|
||||
{
|
||||
role: 'quit' as const,
|
||||
label: i18next.t('menu.quit'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -14,17 +14,31 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { delay } from 'bluebird';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import {
|
||||
BlockDevice,
|
||||
File,
|
||||
Http,
|
||||
Metadata,
|
||||
SourceDestination,
|
||||
} from 'etcher-sdk/build/source-destination';
|
||||
import {
|
||||
MultiDestinationProgress,
|
||||
OnProgressFunction,
|
||||
OnFailFunction,
|
||||
decompressThenFlash,
|
||||
DECOMPRESSED_IMAGE_PREFIX,
|
||||
} from 'etcher-sdk/build/multi-write';
|
||||
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
||||
import * as _ from 'lodash';
|
||||
import * as ipc from 'node-ipc';
|
||||
import { totalmem } from 'os';
|
||||
|
||||
import { File, Http } from 'etcher-sdk/build/source-destination';
|
||||
import { toJSON } from '../../shared/errors';
|
||||
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
|
||||
import { SourceOptions } from '../app/components/source-selector/source-selector';
|
||||
import { delay, isJson } from '../../shared/utils';
|
||||
import { SourceMetadata } from '../app/components/source-selector/source-selector';
|
||||
import axios from 'axios';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID as string;
|
||||
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
||||
@@ -55,8 +69,9 @@ function log(message: string) {
|
||||
/**
|
||||
* @summary Terminate the child writer process
|
||||
*/
|
||||
function terminate(exitCode: number) {
|
||||
async function terminate(exitCode: number) {
|
||||
ipc.disconnect(IPC_SERVER_ID);
|
||||
await cleanupTmpFiles(Date.now(), DECOMPRESSED_IMAGE_PREFIX);
|
||||
process.nextTick(() => {
|
||||
process.exit(exitCode || SUCCESS);
|
||||
});
|
||||
@@ -68,17 +83,28 @@ function terminate(exitCode: number) {
|
||||
async function handleError(error: Error) {
|
||||
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
|
||||
await delay(DISCONNECT_DELAY);
|
||||
terminate(GENERAL_ERROR);
|
||||
await terminate(GENERAL_ERROR);
|
||||
}
|
||||
|
||||
interface WriteResult {
|
||||
bytesWritten: number;
|
||||
devices: {
|
||||
export interface FlashError extends Error {
|
||||
description: string;
|
||||
device: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface WriteResult {
|
||||
bytesWritten?: number;
|
||||
devices?: {
|
||||
failed: number;
|
||||
successful: number;
|
||||
};
|
||||
errors: Array<Error & { device: string }>;
|
||||
sourceMetadata: sdk.sourceDestination.Metadata;
|
||||
errors: FlashError[];
|
||||
sourceMetadata?: Metadata;
|
||||
}
|
||||
|
||||
export interface FlashResults extends WriteResult {
|
||||
skip?: boolean;
|
||||
cancelled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,26 +126,26 @@ async function writeAndValidate({
|
||||
onProgress,
|
||||
onFail,
|
||||
}: {
|
||||
source: sdk.sourceDestination.SourceDestination;
|
||||
destinations: sdk.sourceDestination.BlockDevice[];
|
||||
source: SourceDestination;
|
||||
destinations: BlockDevice[];
|
||||
verify: boolean;
|
||||
autoBlockmapping: boolean;
|
||||
decompressFirst: boolean;
|
||||
onProgress: sdk.multiWrite.OnProgressFunction;
|
||||
onFail: sdk.multiWrite.OnFailFunction;
|
||||
onProgress: OnProgressFunction;
|
||||
onFail: OnFailFunction;
|
||||
}): Promise<WriteResult> {
|
||||
const {
|
||||
sourceMetadata,
|
||||
failures,
|
||||
bytesWritten,
|
||||
} = await sdk.multiWrite.decompressThenFlash({
|
||||
const { sourceMetadata, failures, bytesWritten } = await decompressThenFlash({
|
||||
source,
|
||||
destinations,
|
||||
onFail,
|
||||
onProgress,
|
||||
verify,
|
||||
trim: autoBlockmapping,
|
||||
numBuffers: Math.min(2 + (destinations.length - 1) * 32, 256),
|
||||
numBuffers: Math.min(
|
||||
2 + (destinations.length - 1) * 32,
|
||||
256,
|
||||
Math.floor(totalmem() / 1024 ** 2 / 8),
|
||||
),
|
||||
decompressFirst,
|
||||
});
|
||||
const result: WriteResult = {
|
||||
@@ -132,22 +158,22 @@ async function writeAndValidate({
|
||||
sourceMetadata,
|
||||
};
|
||||
for (const [destination, error] of failures) {
|
||||
const err = error as Error & { device: string };
|
||||
err.device = (destination as sdk.sourceDestination.BlockDevice).device;
|
||||
const err = error as FlashError;
|
||||
const drive = destination as BlockDevice;
|
||||
err.device = drive.device;
|
||||
err.description = drive.description;
|
||||
result.errors.push(err);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface WriteOptions {
|
||||
imagePath: string;
|
||||
image: SourceMetadata;
|
||||
destinations: DrivelistDrive[];
|
||||
unmountOnSuccess: boolean;
|
||||
validateWriteOnSuccess: boolean;
|
||||
autoBlockmapping: boolean;
|
||||
decompressFirst: boolean;
|
||||
source: SourceOptions;
|
||||
SourceType: string;
|
||||
httpRequest?: any;
|
||||
}
|
||||
|
||||
ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
@@ -160,22 +186,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
// no flashing information is available, then it will
|
||||
// assume that the child died halfway through.
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
terminate(SUCCESS);
|
||||
process.once('SIGINT', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
process.once('SIGTERM', () => {
|
||||
terminate(SUCCESS);
|
||||
process.once('SIGTERM', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
// The IPC server failed. Abort.
|
||||
ipc.of[IPC_SERVER_ID].on('error', () => {
|
||||
terminate(SUCCESS);
|
||||
ipc.of[IPC_SERVER_ID].on('error', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
// The IPC server was disconnected. Abort.
|
||||
ipc.of[IPC_SERVER_ID].on('disconnect', () => {
|
||||
terminate(SUCCESS);
|
||||
ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
|
||||
@@ -185,7 +211,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
* @example
|
||||
* writer.on('progress', onProgress)
|
||||
*/
|
||||
const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => {
|
||||
const onProgress = (state: MultiDestinationProgress) => {
|
||||
ipc.of[IPC_SERVER_ID].emit('state', state);
|
||||
};
|
||||
|
||||
@@ -200,11 +226,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
log('Abort');
|
||||
ipc.of[IPC_SERVER_ID].emit('abort');
|
||||
await delay(DISCONNECT_DELAY);
|
||||
terminate(exitCode);
|
||||
await terminate(exitCode);
|
||||
};
|
||||
|
||||
const onSkip = async () => {
|
||||
log('Skip validation');
|
||||
ipc.of[IPC_SERVER_ID].emit('skip');
|
||||
await delay(DISCONNECT_DELAY);
|
||||
await terminate(exitCode);
|
||||
};
|
||||
|
||||
ipc.of[IPC_SERVER_ID].on('cancel', onAbort);
|
||||
|
||||
ipc.of[IPC_SERVER_ID].on('skip', onSkip);
|
||||
|
||||
/**
|
||||
* @summary Failure handler (non-fatal errors)
|
||||
* @param {SourceDestination} destination - destination
|
||||
@@ -212,10 +247,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
* @example
|
||||
* writer.on('fail', onFail)
|
||||
*/
|
||||
const onFail = (
|
||||
destination: sdk.sourceDestination.BlockDevice,
|
||||
error: Error,
|
||||
) => {
|
||||
const onFail = (destination: SourceDestination, error: Error) => {
|
||||
ipc.of[IPC_SERVER_ID].emit('fail', {
|
||||
// TODO: device should be destination
|
||||
// @ts-ignore (destination.drive is private)
|
||||
@@ -224,49 +256,69 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
});
|
||||
};
|
||||
|
||||
const destinations = _.map(options.destinations, 'device');
|
||||
log(`Image: ${options.imagePath}`);
|
||||
const destinations = options.destinations.map((d) => d.device);
|
||||
const imagePath = options.image.path;
|
||||
log(`Image: ${imagePath}`);
|
||||
log(`Devices: ${destinations.join(', ')}`);
|
||||
log(`Umount on success: ${options.unmountOnSuccess}`);
|
||||
log(`Validate on success: ${options.validateWriteOnSuccess}`);
|
||||
log(`Auto blockmapping: ${options.autoBlockmapping}`);
|
||||
log(`Decompress first: ${options.decompressFirst}`);
|
||||
const dests = _.map(options.destinations, (destination) => {
|
||||
return new sdk.sourceDestination.BlockDevice({
|
||||
const dests = options.destinations.map((destination) => {
|
||||
return new BlockDevice({
|
||||
drive: destination,
|
||||
unmountOnSuccess: options.unmountOnSuccess,
|
||||
unmountOnSuccess: true,
|
||||
write: true,
|
||||
direct: true,
|
||||
});
|
||||
});
|
||||
const { SourceType } = options;
|
||||
let source;
|
||||
if (SourceType === File.name) {
|
||||
source = new File({
|
||||
path: options.imagePath,
|
||||
});
|
||||
} else {
|
||||
source = new Http({ url: options.imagePath, avoidRandomAccess: true });
|
||||
}
|
||||
try {
|
||||
let source;
|
||||
if (options.image.drive) {
|
||||
source = new BlockDevice({
|
||||
drive: options.image.drive,
|
||||
direct: !options.autoBlockmapping,
|
||||
});
|
||||
} else {
|
||||
if (SourceType === File.name) {
|
||||
source = new File({
|
||||
path: imagePath,
|
||||
});
|
||||
} else {
|
||||
const decodedImagePath = decodeURIComponent(imagePath);
|
||||
if (isJson(decodedImagePath)) {
|
||||
const imagePathObject = JSON.parse(decodedImagePath);
|
||||
source = new Http({
|
||||
url: imagePathObject.url,
|
||||
avoidRandomAccess: true,
|
||||
axiosInstance: axios.create(_.omit(imagePathObject, ['url'])),
|
||||
auth: options.image.auth,
|
||||
});
|
||||
} else {
|
||||
source = new Http({
|
||||
url: imagePath,
|
||||
avoidRandomAccess: true,
|
||||
auth: options.image.auth,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const results = await writeAndValidate({
|
||||
source,
|
||||
destinations: dests,
|
||||
verify: options.validateWriteOnSuccess,
|
||||
verify: true,
|
||||
autoBlockmapping: options.autoBlockmapping,
|
||||
decompressFirst: options.decompressFirst,
|
||||
onProgress,
|
||||
onFail,
|
||||
});
|
||||
log(`Finish: ${results.bytesWritten}`);
|
||||
results.errors = _.map(results.errors, (error) => {
|
||||
results.errors = results.errors.map((error) => {
|
||||
return toJSON(error);
|
||||
});
|
||||
ipc.of[IPC_SERVER_ID].emit('done', { results });
|
||||
await delay(DISCONNECT_DELAY);
|
||||
terminate(exitCode);
|
||||
} catch (error) {
|
||||
log(`Error: ${error.message}`);
|
||||
await terminate(exitCode);
|
||||
} catch (error: any) {
|
||||
exitCode = GENERAL_ERROR;
|
||||
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ ObjC.import('stdlib')
|
||||
const app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
const result = app.displayDialog('balenaEtcher wants to make changes. Type your password to allow this.', {
|
||||
const result = app.displayDialog('balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.', {
|
||||
defaultAnswer: '',
|
||||
withIcon: 'stop',
|
||||
withIcon: 'caution',
|
||||
buttons: ['Cancel', 'Ok'],
|
||||
defaultButton: 'Ok',
|
||||
hiddenAnswer: true,
|
||||
21
lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env osascript -l JavaScript
|
||||
|
||||
ObjC.import('stdlib')
|
||||
|
||||
const app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
const result = app.displayDialog('balenaEtcher 需要来自管理员的权限才能烧录镜像到磁盘。\n\n输入您的密码以允许此操作。', {
|
||||
defaultAnswer: '',
|
||||
withIcon: 'caution',
|
||||
buttons: ['取消', '好'],
|
||||
defaultButton: '好',
|
||||
hiddenAnswer: true,
|
||||
})
|
||||
|
||||
if (result.buttonReturned === '好') {
|
||||
result.textReturned
|
||||
} else {
|
||||
$.exit(255)
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
*/
|
||||
|
||||
import { execFile } from 'child_process';
|
||||
import { app, remote } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { env } from 'process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { getAppPath } from '../utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||
@@ -29,6 +30,9 @@ export async function sudo(
|
||||
command: string,
|
||||
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
|
||||
try {
|
||||
let lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
lang = lang.substr(0, 2);
|
||||
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
'sudo',
|
||||
['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`],
|
||||
@@ -37,9 +41,9 @@ export async function sudo(
|
||||
env: {
|
||||
PATH: env.PATH,
|
||||
SUDO_ASKPASS: join(
|
||||
(app || remote.app).getAppPath(),
|
||||
getAppPath(),
|
||||
__dirname,
|
||||
'sudo-askpass.osascript.js',
|
||||
'sudo-askpass.osascript-' + lang + '.js',
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -49,7 +53,7 @@ export async function sudo(
|
||||
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
|
||||
stderr,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code === 1) {
|
||||
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
|
||||
return { cancelled: true };
|
||||
|
||||
@@ -14,41 +14,40 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import { Drive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import * as pathIsInside from 'path-is-inside';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
import * as messages from './messages';
|
||||
import { SourceMetadata } from '../gui/app/components/source-selector/source-selector';
|
||||
|
||||
/**
|
||||
* @summary The default unknown size for things such as images and drives
|
||||
*/
|
||||
const UNKNOWN_SIZE = 0;
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is locked
|
||||
*
|
||||
* @description
|
||||
* This usually points out a locked SD Card.
|
||||
*/
|
||||
export function isDriveLocked(drive: DrivelistDrive): boolean {
|
||||
return Boolean(_.get(drive, ['isReadOnly'], false));
|
||||
}
|
||||
export type DrivelistDrive = Drive & {
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
logo: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is a system drive
|
||||
*/
|
||||
export function isSystemDrive(drive: DrivelistDrive): boolean {
|
||||
return Boolean(_.get(drive, ['isSystem'], false));
|
||||
return Boolean(drive.isSystem);
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
path?: string;
|
||||
isSizeEstimated?: boolean;
|
||||
compressedSize?: number;
|
||||
recommendedDriveSize?: number;
|
||||
size?: number;
|
||||
function sourceIsInsideDrive(source: string, drive: DrivelistDrive) {
|
||||
for (const mountpoint of drive.mountpoints || []) {
|
||||
if (pathIsInside(source, mountpoint.path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,11 +59,14 @@ export interface Image {
|
||||
*/
|
||||
export function isSourceDrive(
|
||||
drive: DrivelistDrive,
|
||||
image: Image = {},
|
||||
selection?: SourceMetadata,
|
||||
): boolean {
|
||||
for (const mountpoint of drive.mountpoints || []) {
|
||||
if (image.path !== undefined && pathIsInside(image.path, mountpoint.path)) {
|
||||
return true;
|
||||
if (selection) {
|
||||
if (selection.drive) {
|
||||
return selection.drive.device === drive.device;
|
||||
}
|
||||
if (selection.path) {
|
||||
return sourceIsInsideDrive(selection.path, drive);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -74,17 +76,21 @@ export function isSourceDrive(
|
||||
* @summary Check if a drive is large enough for an image
|
||||
*/
|
||||
export function isDriveLargeEnough(
|
||||
drive: DrivelistDrive | undefined,
|
||||
image: Image,
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
): boolean {
|
||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
||||
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||
|
||||
if (_.get(image, ['isSizeEstimated'])) {
|
||||
if (image === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (image.isSizeEstimated) {
|
||||
// If the drive size is smaller than the original image size, and
|
||||
// the final image size is just an estimation, then we stop right
|
||||
// here, based on the assumption that the final size will never
|
||||
// be less than the original size.
|
||||
if (driveSize < _.get(image, ['compressedSize'], UNKNOWN_SIZE)) {
|
||||
if (driveSize < (image.compressedSize || UNKNOWN_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -95,25 +101,22 @@ export function isDriveLargeEnough(
|
||||
return true;
|
||||
}
|
||||
|
||||
return driveSize >= _.get(image, ['size'], UNKNOWN_SIZE);
|
||||
return driveSize >= (image.size || UNKNOWN_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is disabled (i.e. not ready for selection)
|
||||
* @summary Check if a drive is valid, i.e. large enough for an image
|
||||
*/
|
||||
export function isDriveDisabled(drive: DrivelistDrive): boolean {
|
||||
return _.get(drive, ['disabled'], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is valid, i.e. not locked and large enough for an image
|
||||
*/
|
||||
export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
|
||||
export function isDriveValid(
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
write: boolean = true,
|
||||
): boolean {
|
||||
return (
|
||||
!isDriveLocked(drive) &&
|
||||
isDriveLargeEnough(drive, image) &&
|
||||
!isSourceDrive(drive, image) &&
|
||||
!isDriveDisabled(drive)
|
||||
!write ||
|
||||
(!drive.disabled &&
|
||||
isDriveLargeEnough(drive, image) &&
|
||||
!isSourceDrive(drive, image as SourceMetadata))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,23 +127,23 @@ export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
|
||||
* If the image doesn't have a recommended size, this function returns true.
|
||||
*/
|
||||
export function isDriveSizeRecommended(
|
||||
drive: DrivelistDrive | undefined,
|
||||
image: Image,
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
): boolean {
|
||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
||||
return driveSize >= _.get(image, ['recommendedDriveSize'], UNKNOWN_SIZE);
|
||||
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||
return driveSize >= (image?.recommendedDriveSize || UNKNOWN_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary 64GB
|
||||
* @summary 128GB
|
||||
*/
|
||||
export const LARGE_DRIVE_SIZE = 64e9;
|
||||
export const LARGE_DRIVE_SIZE = 128e9;
|
||||
|
||||
/**
|
||||
* @summary Check whether a drive's size is 'large'
|
||||
*/
|
||||
export function isDriveSizeLarge(drive?: DrivelistDrive): boolean {
|
||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
||||
export function isDriveSizeLarge(drive: DrivelistDrive): boolean {
|
||||
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||
return driveSize > LARGE_DRIVE_SIZE;
|
||||
}
|
||||
|
||||
@@ -155,6 +158,33 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
||||
ERROR: 2,
|
||||
};
|
||||
|
||||
export const statuses = {
|
||||
locked: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.locked(),
|
||||
},
|
||||
system: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.system(),
|
||||
},
|
||||
containsImage: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.containsImage(),
|
||||
},
|
||||
large: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.largeDrive(),
|
||||
},
|
||||
small: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.tooSmall(),
|
||||
},
|
||||
sizeNotRecommended: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.sizeNotRecommended(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get drive/image compatibility in an object
|
||||
*
|
||||
@@ -167,56 +197,42 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
||||
*/
|
||||
export function getDriveImageCompatibilityStatuses(
|
||||
drive: DrivelistDrive,
|
||||
image: Image = {},
|
||||
image: SourceMetadata | undefined,
|
||||
write: boolean,
|
||||
) {
|
||||
const statusList = [];
|
||||
|
||||
// Mind the order of the if-statements if you modify.
|
||||
if (isDriveLocked(drive)) {
|
||||
if (drive.isReadOnly && write) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.locked(),
|
||||
});
|
||||
} else if (
|
||||
}
|
||||
if (
|
||||
!_.isNil(drive) &&
|
||||
!_.isNil(drive.size) &&
|
||||
!isDriveLargeEnough(drive, image)
|
||||
) {
|
||||
const imageSize = (image.isSizeEstimated
|
||||
? image.compressedSize
|
||||
: image.size) as number;
|
||||
const relativeBytes = imageSize - drive.size;
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
|
||||
});
|
||||
statusList.push(statuses.small);
|
||||
} else {
|
||||
if (isSourceDrive(drive, image)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.containsImage(),
|
||||
});
|
||||
}
|
||||
|
||||
// Avoid showing "large drive" with "system drive" status
|
||||
if (isSystemDrive(drive)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.system(),
|
||||
});
|
||||
statusList.push(statuses.system);
|
||||
} else if (isDriveSizeLarge(drive)) {
|
||||
statusList.push(statuses.large);
|
||||
}
|
||||
|
||||
if (isDriveSizeLarge(drive)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.largeDrive(),
|
||||
});
|
||||
if (isSourceDrive(drive, image as SourceMetadata)) {
|
||||
statusList.push(statuses.containsImage);
|
||||
}
|
||||
|
||||
if (!_.isNil(drive) && !isDriveSizeRecommended(drive, image)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.sizeNotRecommended(),
|
||||
});
|
||||
if (
|
||||
image !== undefined &&
|
||||
!_.isNil(drive) &&
|
||||
!isDriveSizeRecommended(drive, image)
|
||||
) {
|
||||
statusList.push(statuses.sizeNotRecommended);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,10 +248,11 @@ export function getDriveImageCompatibilityStatuses(
|
||||
*/
|
||||
export function getListDriveImageCompatibilityStatuses(
|
||||
drives: DrivelistDrive[],
|
||||
image: Image,
|
||||
image: SourceMetadata | undefined,
|
||||
write: boolean,
|
||||
) {
|
||||
return _.flatMap(drives, (drive) => {
|
||||
return getDriveImageCompatibilityStatuses(drive, image);
|
||||
return drives.flatMap((drive) => {
|
||||
return getDriveImageCompatibilityStatuses(drive, image, write);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,36 +264,15 @@ export function getListDriveImageCompatibilityStatuses(
|
||||
*/
|
||||
export function hasDriveImageCompatibilityStatus(
|
||||
drive: DrivelistDrive,
|
||||
image: Image,
|
||||
image: SourceMetadata | undefined,
|
||||
write: boolean,
|
||||
) {
|
||||
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
|
||||
return Boolean(
|
||||
getDriveImageCompatibilityStatuses(drive, image, write).length,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Does any drive/image pair have at least one compatibility status?
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* Given an image and a drive, return whether they have a connected compatibility status object.
|
||||
*
|
||||
* @param {Object[]} drives - drives
|
||||
* @param {Object} image - image
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* if (constraints.hasDriveImageCompatibilityStatus(drive, image)) {
|
||||
* console.log('This drive-image pair has a compatibility status message!')
|
||||
* }
|
||||
*/
|
||||
export function hasListDriveImageCompatibilityStatus(
|
||||
drives: DrivelistDrive[],
|
||||
image: Image,
|
||||
) {
|
||||
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
|
||||
}
|
||||
|
||||
export interface TargetStatus {
|
||||
export interface DriveStatus {
|
||||
message: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
@@ -14,48 +14,37 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
|
||||
function createErrorDetails(options: {
|
||||
title: string | ((error: Error) => string);
|
||||
description: string | ((error: Error) => string);
|
||||
}): {
|
||||
title: (error: Error) => string;
|
||||
description: (error: Error) => string;
|
||||
} {
|
||||
return _.pick(
|
||||
_.mapValues(options, (value) => {
|
||||
return _.isFunction(value) ? value : _.constant(value);
|
||||
}),
|
||||
['title', 'description'],
|
||||
);
|
||||
}
|
||||
export type ErrorWithPath = Error & {
|
||||
path?: string;
|
||||
code?: keyof typeof HUMAN_FRIENDLY;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Human-friendly error messages
|
||||
*/
|
||||
export const HUMAN_FRIENDLY = {
|
||||
ENOENT: createErrorDetails({
|
||||
title: (error: Error & { path: string }) => {
|
||||
ENOENT: {
|
||||
title: (error: ErrorWithPath) => {
|
||||
return `No such file or directory: ${error.path}`;
|
||||
},
|
||||
description: "The file you're trying to access doesn't exist",
|
||||
}),
|
||||
EPERM: createErrorDetails({
|
||||
title: "You're not authorized to perform this operation",
|
||||
description: 'Please ensure you have necessary permissions for this task',
|
||||
}),
|
||||
EACCES: createErrorDetails({
|
||||
title: "You don't have access to this resource",
|
||||
description:
|
||||
description: () => "The file you're trying to access doesn't exist",
|
||||
},
|
||||
EPERM: {
|
||||
title: () => "You're not authorized to perform this operation",
|
||||
description: () =>
|
||||
'Please ensure you have necessary permissions for this task',
|
||||
},
|
||||
EACCES: {
|
||||
title: () => "You don't have access to this resource",
|
||||
description: () =>
|
||||
'Please ensure you have necessary permissions to access this resource',
|
||||
}),
|
||||
ENOMEM: createErrorDetails({
|
||||
title: 'Your system ran out of memory',
|
||||
description:
|
||||
},
|
||||
ENOMEM: {
|
||||
title: () => 'Your system ran out of memory',
|
||||
description: () =>
|
||||
'Please make sure your system has enough available memory for this task',
|
||||
}),
|
||||
};
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @summary Get user friendly property from an error
|
||||
@@ -71,19 +60,21 @@ export const HUMAN_FRIENDLY = {
|
||||
* }
|
||||
*/
|
||||
function getUserFriendlyMessageProperty(
|
||||
error: Error,
|
||||
error: ErrorWithPath,
|
||||
property: 'title' | 'description',
|
||||
): string | null {
|
||||
const code = _.get(error, ['code']);
|
||||
|
||||
if (_.isNil(code) || !_.isString(code)) {
|
||||
return null;
|
||||
): string | undefined {
|
||||
if (typeof error.code !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return _.invoke(HUMAN_FRIENDLY, [code, property], error);
|
||||
return HUMAN_FRIENDLY[error.code]?.[property]?.(error);
|
||||
}
|
||||
|
||||
const isBlank = _.flow([_.trim, _.isEmpty]);
|
||||
function isBlank(s: string | number | null | undefined) {
|
||||
if (typeof s === 'number') {
|
||||
s = s.toString();
|
||||
}
|
||||
return (s ?? '').trim() === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the title of an error
|
||||
@@ -92,23 +83,19 @@ const isBlank = _.flow([_.trim, _.isEmpty]);
|
||||
* Try to get as much information as possible about the error
|
||||
* rather than falling back to generic messages right away.
|
||||
*/
|
||||
export function getTitle(error: Error): string {
|
||||
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
|
||||
return _.toString(error);
|
||||
}
|
||||
|
||||
export function getTitle(error: ErrorWithPath): string {
|
||||
const codeTitle = getUserFriendlyMessageProperty(error, 'title');
|
||||
if (!_.isNil(codeTitle)) {
|
||||
if (codeTitle !== undefined) {
|
||||
return codeTitle;
|
||||
}
|
||||
|
||||
const message = _.get(error, ['message']);
|
||||
const message = error.message;
|
||||
if (!isBlank(message)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const code = _.get(error, ['code']);
|
||||
if (!_.isNil(code) && !isBlank(code)) {
|
||||
const code = error.code;
|
||||
if (!isBlank(code)) {
|
||||
return `Error code: ${code}`;
|
||||
}
|
||||
|
||||
@@ -119,40 +106,19 @@ export function getTitle(error: Error): string {
|
||||
* @summary Get the description of an error
|
||||
*/
|
||||
export function getDescription(
|
||||
error: Error & { description?: string },
|
||||
options: { userFriendlyDescriptionsOnly?: boolean } = {},
|
||||
error: ErrorWithPath & { description?: string },
|
||||
): string {
|
||||
_.defaults(options, {
|
||||
userFriendlyDescriptionsOnly: false,
|
||||
});
|
||||
|
||||
if (!_.isError(error) && !_.isPlainObject(error)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isBlank(error.description)) {
|
||||
return error.description as string;
|
||||
}
|
||||
|
||||
const codeDescription = getUserFriendlyMessageProperty(error, 'description');
|
||||
if (!_.isNil(codeDescription)) {
|
||||
if (codeDescription !== undefined) {
|
||||
return codeDescription;
|
||||
}
|
||||
|
||||
if (options.userFriendlyDescriptionsOnly) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (error.stack) {
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
if (_.isEmpty(error)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const INDENTATION_SPACES = 2;
|
||||
return JSON.stringify(error, null, INDENTATION_SPACES);
|
||||
return JSON.stringify(error, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,24 +128,24 @@ export function createError(options: {
|
||||
title: string;
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
}): Error & { description?: string; report?: boolean; code?: string } {
|
||||
code?: keyof typeof HUMAN_FRIENDLY;
|
||||
}): ErrorWithPath & { description?: string; report?: boolean } {
|
||||
if (isBlank(options.title)) {
|
||||
throw new Error(`Invalid error title: ${options.title}`);
|
||||
}
|
||||
|
||||
const error: Error & {
|
||||
const error: ErrorWithPath & {
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
} = new Error(options.title);
|
||||
error.description = options.description;
|
||||
|
||||
if (!_.isNil(options.report) && !options.report) {
|
||||
if (options.report === false) {
|
||||
error.report = false;
|
||||
}
|
||||
|
||||
if (!_.isNil(options.code)) {
|
||||
if (options.code !== undefined) {
|
||||
error.code = options.code;
|
||||
}
|
||||
|
||||
@@ -198,7 +164,7 @@ export function createError(options: {
|
||||
export function createUserError(options: {
|
||||
title: string;
|
||||
description: string;
|
||||
code?: string;
|
||||
code?: keyof typeof HUMAN_FRIENDLY;
|
||||
}): Error {
|
||||
return createError({
|
||||
title: options.title,
|
||||
@@ -208,13 +174,6 @@ export function createUserError(options: {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if an error is an user error
|
||||
*/
|
||||
export function isUserError(error: Error & { report?: boolean }): boolean {
|
||||
return _.isNil(error.report) ? false : !error.report;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Convert an Error object to a JSON object
|
||||
* @function
|
||||
@@ -260,5 +219,5 @@ export function toJSON(
|
||||
* @summary Convert a JSON object to an Error object
|
||||
*/
|
||||
export function fromJSON(json: any): Error {
|
||||
return _.assign(new Error(json.message), json);
|
||||
return Object.assign(new Error(json.message), json);
|
||||
}
|
||||
|
||||
@@ -15,16 +15,18 @@
|
||||
*/
|
||||
|
||||
import { Dictionary } from 'lodash';
|
||||
import { outdent } from 'outdent';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import '../gui/app/i18n';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export const progress: Dictionary<(quantity: number) => string> = {
|
||||
successful: (quantity: number) => {
|
||||
const plural = quantity === 1 ? '' : 's';
|
||||
return `Successful device${plural}`;
|
||||
return i18next.t('message.flashSucceed', { count: quantity });
|
||||
},
|
||||
|
||||
failed: (quantity: number) => {
|
||||
const plural = quantity === 1 ? '' : 's';
|
||||
return `Failed device${plural}`;
|
||||
return i18next.t('message.flashFail', { count: quantity });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,122 +38,121 @@ export const info = {
|
||||
) => {
|
||||
const targets = [];
|
||||
if (failed + successful === 1) {
|
||||
targets.push(`to ${drive.description} (${drive.displayName})`);
|
||||
targets.push(
|
||||
i18next.t('message.toDrive', {
|
||||
description: drive.description,
|
||||
name: drive.displayName,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
if (successful) {
|
||||
const plural = successful === 1 ? '' : 's';
|
||||
targets.push(`to ${successful} target${plural}`);
|
||||
targets.push(
|
||||
i18next.t('message.toTarget', {
|
||||
count: successful,
|
||||
num: successful,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (failed) {
|
||||
const plural = failed === 1 ? '' : 's';
|
||||
targets.push(`and failed to be flashed to ${failed} target${plural}`);
|
||||
targets.push(
|
||||
i18next.t('message.andFailTarget', { count: failed, num: failed }),
|
||||
);
|
||||
}
|
||||
}
|
||||
return `${imageBasename} was successfully flashed ${targets.join(' ')}`;
|
||||
return i18next.t('message.succeedTo', {
|
||||
name: imageBasename,
|
||||
target: targets.join(' '),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const compatibility = {
|
||||
sizeNotRecommended: () => {
|
||||
return 'Not Recommended';
|
||||
return i18next.t('message.sizeNotRecommended');
|
||||
},
|
||||
|
||||
tooSmall: (additionalSpace: string) => {
|
||||
return `Insufficient space, additional ${additionalSpace} required`;
|
||||
tooSmall: () => {
|
||||
return i18next.t('message.tooSmall');
|
||||
},
|
||||
|
||||
locked: () => {
|
||||
return 'Locked';
|
||||
return i18next.t('message.locked');
|
||||
},
|
||||
|
||||
system: () => {
|
||||
return 'System drive';
|
||||
return i18next.t('message.system');
|
||||
},
|
||||
|
||||
containsImage: () => {
|
||||
return 'Source drive';
|
||||
return i18next.t('message.containsImage');
|
||||
},
|
||||
|
||||
// The drive is large and therefore likely not a medium you want to write to.
|
||||
largeDrive: () => {
|
||||
return 'Large drive';
|
||||
return i18next.t('message.largeDrive');
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const warning = {
|
||||
unrecommendedDriveSize: (
|
||||
image: { recommendedDriveSize: number },
|
||||
drive: { device: string; size: number },
|
||||
) => {
|
||||
return [
|
||||
`This image recommends a ${image.recommendedDriveSize}`,
|
||||
`bytes drive, however ${drive.device} is only ${drive.size} bytes.`,
|
||||
].join(' ');
|
||||
tooSmall: (source: { size: number }, target: { size: number }) => {
|
||||
return outdent({ newline: ' ' })`
|
||||
${i18next.t('message.sourceLarger', {
|
||||
byte: prettyBytes(source.size - target.size),
|
||||
})}
|
||||
`;
|
||||
},
|
||||
|
||||
exitWhileFlashing: () => {
|
||||
return [
|
||||
'You are currently flashing a drive.',
|
||||
'Closing Etcher may leave your drive in an unusable state.',
|
||||
].join(' ');
|
||||
return i18next.t('message.exitWhileFlashing');
|
||||
},
|
||||
|
||||
looksLikeWindowsImage: () => {
|
||||
return [
|
||||
'It looks like you are trying to burn a Windows image.\n\n',
|
||||
'Unlike other images, Windows images require special processing to be made bootable.',
|
||||
'We suggest you use a tool specially designed for this purpose, such as',
|
||||
'<a href="https://rufus.akeo.ie">Rufus</a> (Windows),',
|
||||
'<a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux),',
|
||||
'or Boot Camp Assistant (macOS).',
|
||||
].join(' ');
|
||||
return i18next.t('message.looksLikeWindowsImage');
|
||||
},
|
||||
|
||||
missingPartitionTable: () => {
|
||||
return [
|
||||
'It looks like this is not a bootable image.\n\n',
|
||||
'The image does not appear to contain a partition table,',
|
||||
'and might not be recognized or bootable by your device.',
|
||||
].join(' ');
|
||||
return i18next.t('message.missingPartitionTable', {
|
||||
type: i18next.t('message.image'),
|
||||
});
|
||||
},
|
||||
|
||||
largeDriveSize: (drive: { description: string; device: string }) => {
|
||||
return [
|
||||
`Drive ${drive.description} (${drive.device}) is unusually large for an SD card or USB stick.`,
|
||||
'\n\nAre you sure you want to flash this drive?',
|
||||
].join(' ');
|
||||
driveMissingPartitionTable: () => {
|
||||
return i18next.t('message.missingPartitionTable', {
|
||||
type: i18next.t('message.drive'),
|
||||
});
|
||||
},
|
||||
|
||||
largeDriveSize: () => {
|
||||
return i18next.t('message.largeDriveSize');
|
||||
},
|
||||
|
||||
systemDrive: () => {
|
||||
return i18next.t('message.systemDrive');
|
||||
},
|
||||
|
||||
sourceDrive: () => {
|
||||
return i18next.t('message.sourceDrive');
|
||||
},
|
||||
};
|
||||
|
||||
export const error = {
|
||||
notEnoughSpaceInDrive: () => {
|
||||
return [
|
||||
'Not enough space on the drive.',
|
||||
'Please insert larger one and try again.',
|
||||
].join(' ');
|
||||
return i18next.t('message.noSpace');
|
||||
},
|
||||
|
||||
genericFlashError: (err: Error) => {
|
||||
return `Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n${err.message}`;
|
||||
return i18next.t('message.genericFlashError', { error: err.message });
|
||||
},
|
||||
|
||||
validation: () => {
|
||||
return [
|
||||
'The write has been completed successfully but Etcher detected potential',
|
||||
'corruption issues when reading the image back from the drive.',
|
||||
'\n\nPlease consider writing the image to a different drive.',
|
||||
].join(' ');
|
||||
return i18next.t('message.validation');
|
||||
},
|
||||
|
||||
openImage: (imageBasename: string, errorMessage: string) => {
|
||||
return [
|
||||
`Something went wrong while opening ${imageBasename}\n\n`,
|
||||
`Error: ${errorMessage}`,
|
||||
].join('');
|
||||
},
|
||||
|
||||
elevationRequired: () => {
|
||||
return 'This should should be run with root/administrator permissions.';
|
||||
openSource: (sourceName: string, errorMessage: string) => {
|
||||
return i18next.t('message.openError', {
|
||||
source: sourceName,
|
||||
error: errorMessage,
|
||||
});
|
||||
},
|
||||
|
||||
flashFailure: (
|
||||
@@ -160,35 +161,33 @@ export const error = {
|
||||
) => {
|
||||
const target =
|
||||
drives.length === 1
|
||||
? `${drives[0].description} (${drives[0].displayName})`
|
||||
: `${drives.length} targets`;
|
||||
return `Something went wrong while writing ${imageBasename} to ${target}.`;
|
||||
? i18next.t('message.toDrive', {
|
||||
description: drives[0].description,
|
||||
name: drives[0].displayName,
|
||||
})
|
||||
: i18next.t('message.toTarget', {
|
||||
count: drives.length,
|
||||
num: drives.length,
|
||||
});
|
||||
return i18next.t('message.flashError', {
|
||||
image: imageBasename,
|
||||
targets: target,
|
||||
});
|
||||
},
|
||||
|
||||
driveUnplugged: () => {
|
||||
return [
|
||||
'Looks like Etcher lost access to the drive.',
|
||||
'Did it get unplugged accidentally?',
|
||||
"\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
|
||||
].join(' ');
|
||||
return i18next.t('message.unplug');
|
||||
},
|
||||
|
||||
inputOutput: () => {
|
||||
return [
|
||||
'Looks like Etcher is not able to write to this location of the drive.',
|
||||
'This error is usually caused by a faulty drive, reader, or port.',
|
||||
'\n\nPlease try again with another drive, reader, or port.',
|
||||
].join(' ');
|
||||
return i18next.t('message.cannotWrite');
|
||||
},
|
||||
|
||||
childWriterDied: () => {
|
||||
return [
|
||||
'The writer process ended unexpectedly.',
|
||||
'Please try again, and contact the Etcher team if the problem persists.',
|
||||
].join(' ');
|
||||
return i18next.t('message.childWriterDied');
|
||||
},
|
||||
|
||||
unsupportedProtocol: () => {
|
||||
return 'Only http:// and https:// URLs are supported.';
|
||||
return i18next.t('message.badProtocol');
|
||||
},
|
||||
};
|
||||
|
||||