Compare commits
1310 Commits
v1.4.2
...
vipul/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82d0bba96a | ||
|
|
5945ab1f50 | ||
|
|
59d67220d4 | ||
|
|
61610ded84 | ||
|
|
c87a132f40 | ||
|
|
350d4de32b | ||
|
|
f5f9025d6d | ||
|
|
549d744d04 | ||
|
|
6194460dc2 | ||
|
|
8370f638b4 | ||
|
|
ac34c51125 | ||
|
|
b241470fe1 | ||
|
|
179697040c | ||
|
|
335766ed12 | ||
|
|
4c5d052a71 | ||
|
|
86423342a8 | ||
|
|
d8b41552e3 | ||
|
|
11c65fb392 | ||
|
|
bed126506f | ||
|
|
f6aeb52b16 | ||
|
|
a5201942b8 | ||
|
|
c1f7164273 | ||
|
|
6774bf784c | ||
|
|
56ec8b4eac | ||
|
|
35868509af | ||
|
|
3ab6749f49 | ||
|
|
7a012a92bc | ||
|
|
aba01825a0 | ||
|
|
907a3308de | ||
|
|
4366bb372f | ||
|
|
a6f6cd4a19 | ||
|
|
03ee428039 | ||
|
|
8d652d064d | ||
|
|
28adc34239 | ||
|
|
120e9bf42f | ||
|
|
59f54e194b | ||
|
|
c4834e61a7 | ||
|
|
e4d02bc561 | ||
|
|
b9e54e39f7 | ||
|
|
f3c32eac65 | ||
|
|
9a303ab344 | ||
|
|
9c1b55bebc | ||
|
|
30ae4bbd86 | ||
|
|
c6126a980a | ||
|
|
ef90d048ca | ||
|
|
b938132038 | ||
|
|
3cb2e78fe7 | ||
|
|
ea9875ddf0 | ||
|
|
65dacd2ff2 | ||
|
|
a190818827 | ||
|
|
98e33b619b | ||
|
|
685ed715ac | ||
|
|
3cf3c4b398 | ||
|
|
1c2ef4b1d4 | ||
|
|
d22fc91585 | ||
|
|
0a28af5c35 | ||
|
|
0c1e5b88ef | ||
|
|
790201be90 | ||
|
|
d8d379f05e | ||
|
|
b5e9701048 | ||
|
|
292f86d6f5 | ||
|
|
76ca9934c8 | ||
|
|
37b826ee4e | ||
|
|
1e1bd3c508 | ||
|
|
00e8f11913 | ||
|
|
a3c24a26a0 | ||
|
|
4232928ad8 | ||
|
|
b165fb78da | ||
|
|
e9f6c5ead9 | ||
|
|
b2d0c1c9dd | ||
|
|
14d91400a4 | ||
|
|
d0114aece7 | ||
|
|
dff2df4aab | ||
|
|
13159f93ee | ||
|
|
3ece1fd841 | ||
|
|
f46963b6b3 | ||
|
|
b97f4e0031 | ||
|
|
e2d233d74b | ||
|
|
a7ca2e527b | ||
|
|
396a053c0a | ||
|
|
d1a3f1cb88 | ||
|
|
9f96558cdd | ||
|
|
b3bc589d70 | ||
|
|
18d2c28110 | ||
|
|
b272ef296d | ||
|
|
32ca28a3a9 | ||
|
|
4d5e5a3b0b | ||
|
|
8b3f37102d | ||
|
|
4b74253631 | ||
|
|
a81b552b95 | ||
|
|
53f53c0f75 | ||
|
|
fdaf5c69d6 | ||
|
|
061afca5d3 | ||
|
|
ccb08a48f1 | ||
|
|
a8f3d45b12 | ||
|
|
7e333caaf9 | ||
|
|
70229e8684 | ||
|
|
261700389b | ||
|
|
250aed2eb1 | ||
|
|
ed1f008fe2 | ||
|
|
e9ce270dab | ||
|
|
1ee110bc95 | ||
|
|
33dd07c675 | ||
|
|
39ccbbeeda | ||
|
|
55d2400ac7 | ||
|
|
0bdea5c54c | ||
|
|
3be372d49f | ||
|
|
d0c66b2c48 | ||
|
|
65082c4790 | ||
|
|
e87ed9beed | ||
|
|
bc5563d9c2 | ||
|
|
ad83ab5dcc | ||
|
|
0dc1cf9701 | ||
|
|
11489c6538 | ||
|
|
2619d4bc86 | ||
|
|
3730efd350 | ||
|
|
6ece32c546 | ||
|
|
fd9996a3cc | ||
|
|
f06cc89152 | ||
|
|
c1d7ab3fa9 | ||
|
|
b206483c7c | ||
|
|
c3eb8c7b56 | ||
|
|
0849d4f435 | ||
|
|
1dba3ae19b | ||
|
|
f33f2e3771 | ||
|
|
e56aaed973 | ||
|
|
a4659f038e | ||
|
|
cd462818da | ||
|
|
37769efbed | ||
|
|
0f70c4bbce | ||
|
|
48b5e8b9d9 | ||
|
|
1f138f0ecc | ||
|
|
73f67e99ca | ||
|
|
9114da2445 | ||
|
|
554bbcc780 | ||
|
|
4db2289cfd | ||
|
|
c15b56bc23 | ||
|
|
9f52dda6ae | ||
|
|
fadcefb11a | ||
|
|
361c32913c | ||
|
|
5c2042198e | ||
|
|
99df53098c | ||
|
|
aa563c87bd | ||
|
|
1188888956 | ||
|
|
f9d7991dc8 | ||
|
|
53954e81fd | ||
|
|
f82996bfd1 | ||
|
|
b74069eb41 | ||
|
|
e8c7591751 | ||
|
|
3521b61a81 | ||
|
|
93db90c725 | ||
|
|
1dc56aed14 | ||
|
|
d814202424 | ||
|
|
c54856a616 | ||
|
|
fc45df270a | ||
|
|
3cde2faed0 | ||
|
|
b4b8c89aad | ||
|
|
36d05724c0 | ||
|
|
b1e4e681d1 | ||
|
|
3987078c11 | ||
|
|
de0010eb72 | ||
|
|
1f94f44b18 | ||
|
|
fe0b45cae6 | ||
|
|
c32e485f27 | ||
|
|
409b78fc21 | ||
|
|
2f08142f5a | ||
|
|
8c4edaabba | ||
|
|
05497ce85c | ||
|
|
d3df2fe57e | ||
|
|
a0f07082f2 | ||
|
|
b7efa8e1f0 | ||
|
|
3647457bb5 | ||
|
|
2e5a39dcd8 | ||
|
|
edabacfb3a | ||
|
|
f46176fd10 | ||
|
|
2158e20380 | ||
|
|
fa593e33d1 | ||
|
|
50730bd3df | ||
|
|
4e68955981 | ||
|
|
3c0084d012 | ||
|
|
8bd11a01ae | ||
|
|
da3a22d0f6 | ||
|
|
e708212d41 | ||
|
|
a5ceba8435 | ||
|
|
446e8e1253 | ||
|
|
c69b2fa053 | ||
|
|
0597c0e908 | ||
|
|
af2b6bc8ca | ||
|
|
a2c7a542df | ||
|
|
e37ae2743f | ||
|
|
644d955f08 | ||
|
|
e7b4f09021 | ||
|
|
1e0a6a3129 | ||
|
|
ef3b8915d8 | ||
|
|
e58cfd89c5 | ||
|
|
1c52379ee3 | ||
|
|
e2c2b40690 | ||
|
|
bddb89e4a1 | ||
|
|
560ed91e2e | ||
|
|
1f8f7ad7f8 | ||
|
|
a2a0f2ef41 | ||
|
|
40e5fb2287 | ||
|
|
6c49c71b3f | ||
|
|
deb3db0fff | ||
|
|
4872fa3d6e | ||
|
|
640a7409ee | ||
|
|
a7637ad8d4 | ||
|
|
31409c61ca | ||
|
|
e74dc9eb60 | ||
|
|
06997fdf29 | ||
|
|
611e659626 | ||
|
|
e484ae9837 | ||
|
|
7e7ca9524e | ||
|
|
db09b7440d | ||
|
|
e9603505d2 | ||
|
|
0f45f6aca1 | ||
|
|
0a28a7794d | ||
|
|
7c2644ec51 | ||
|
|
ae62812c61 | ||
|
|
68e24df52b | ||
|
|
b9076d01af | ||
|
|
78a5339e3e | ||
|
|
b099770cb1 | ||
|
|
b76366a514 | ||
|
|
eeab351636 | ||
|
|
3e45691d0b | ||
|
|
f9d79521a1 | ||
|
|
14a89b3b8a | ||
|
|
8fa6e618c4 | ||
|
|
093008dee7 | ||
|
|
42838eba09 | ||
|
|
aa72c5d3bb | ||
|
|
bb04098062 | ||
|
|
dda022df37 | ||
|
|
377dfb8e22 | ||
|
|
07befd0bd1 | ||
|
|
2635a410df | ||
|
|
5e5f82c4b5 | ||
|
|
991cbf6b7f | ||
|
|
688d697a99 | ||
|
|
7894a67719 | ||
|
|
7a7ea74984 | ||
|
|
12cd8a39c1 | ||
|
|
2c07538f8f | ||
|
|
c9bfd350ed | ||
|
|
a485d2b4df | ||
|
|
8ed5ff25a5 | ||
|
|
a17a919c37 | ||
|
|
55cafb9268 | ||
|
|
92dfdc6edd | ||
|
|
fff9452509 | ||
|
|
27e560c961 | ||
|
|
34489f0d66 | ||
|
|
b7f8c8368c | ||
|
|
f383f0be6c | ||
|
|
ff08cb44f9 | ||
|
|
6cb914e969 | ||
|
|
a24be20e95 | ||
|
|
08716efbd5 | ||
|
|
24c8ede746 | ||
|
|
548475996c | ||
|
|
7f9add3f1e | ||
|
|
6eab47259e | ||
|
|
46663e3a6f | ||
|
|
9797a2152d | ||
|
|
a7c3431556 | ||
|
|
fef9cd7bec | ||
|
|
b2c4f7a250 | ||
|
|
88ae9fcbd1 | ||
|
|
bc092114c1 | ||
|
|
9f29dc8b76 | ||
|
|
5fbaa3a3db | ||
|
|
0c59168ceb | ||
|
|
540fe90609 | ||
|
|
1f44f3944f | ||
|
|
fbacb8187d | ||
|
|
ac2d4ae8f3 | ||
|
|
a3322e9fd7 | ||
|
|
281f119456 | ||
|
|
140f3452ed | ||
|
|
481be42eb5 | ||
|
|
f2a37079eb | ||
|
|
76fa698995 | ||
|
|
f8e21e2338 | ||
|
|
482c29bc2a | ||
|
|
0bf1ec4958 | ||
|
|
3b105d5a6a | ||
|
|
6d9c81da43 | ||
|
|
c2e23855b3 | ||
|
|
3f59d35fb6 | ||
|
|
44c74f33d9 | ||
|
|
512785e0a9 | ||
|
|
963fc574c3 | ||
|
|
3218fc2c83 | ||
|
|
dc9351713c | ||
|
|
e72049d6e8 | ||
|
|
170126a490 | ||
|
|
7d53d0aadc | ||
|
|
5eac622b8c | ||
|
|
175e41de8d | ||
|
|
61f4762341 | ||
|
|
7c24d1486f | ||
|
|
630f6c691c | ||
|
|
5c5273bd6c | ||
|
|
9bde38df5a | ||
|
|
391e4444d4 | ||
|
|
e5ee0f1961 | ||
|
|
c8737806c0 | ||
|
|
953f572b53 | ||
|
|
05d0f7142d | ||
|
|
ba29d76a00 | ||
|
|
692274691e | ||
|
|
394d3e0bf2 | ||
|
|
784dd03ba7 | ||
|
|
8560189a1e | ||
|
|
098ca9a9a1 | ||
|
|
3ca50a1e2d | ||
|
|
00f193541d | ||
|
|
8ce9eac704 | ||
|
|
76086a8f91 | ||
|
|
9b71772e35 | ||
|
|
72e5631167 | ||
|
|
339c7d56bd | ||
|
|
ba16995070 | ||
|
|
b32c4ee728 | ||
|
|
14e4cbf749 | ||
|
|
406955ca3e | ||
|
|
5a45f8b122 | ||
|
|
129e7e20e8 | ||
|
|
7165a8190b | ||
|
|
07fde0d73f | ||
|
|
a360370c4e | ||
|
|
92cd3d688d | ||
|
|
6554ccf0f8 | ||
|
|
9444f0e1b1 | ||
|
|
d63f5eca0d | ||
|
|
e39fed1f25 | ||
|
|
2dc359b19c | ||
|
|
7aec8a4ae2 | ||
|
|
af9d3ba9f1 | ||
|
|
b0c71b21b3 | ||
|
|
71c7fbd3a2 | ||
|
|
f8cc7c36b4 | ||
|
|
5d95fcb81f | ||
|
|
d481536a3f | ||
|
|
62b42e9254 | ||
|
|
03e3354d50 | ||
|
|
f01f1ddd7a | ||
|
|
2cb58bbbf0 | ||
|
|
2aedea3139 | ||
|
|
59e37182be | ||
|
|
52bdd02a4b | ||
|
|
b1376dfa73 | ||
|
|
37ed18c38b | ||
|
|
b7ad7bd729 | ||
|
|
b43ec4414e | ||
|
|
f05f9d33f9 | ||
|
|
fcc9c5e577 | ||
|
|
3259a8206f | ||
|
|
3fa9611971 | ||
|
|
b749c2d45a | ||
|
|
29e2e9c657 | ||
|
|
f983d88e52 | ||
|
|
1449478c5b | ||
|
|
7e7a669116 | ||
|
|
28f9954661 | ||
|
|
b7e82f7694 | ||
|
|
f0bbd1a1cd | ||
|
|
5f5c66e3f2 | ||
|
|
2fc8b07e29 | ||
|
|
bdb1690a49 | ||
|
|
10b028355f | ||
|
|
a4366556c0 | ||
|
|
9c25cc663a | ||
|
|
ba21da4f0b | ||
|
|
34349f64d5 | ||
|
|
f5c7dc932a | ||
|
|
4880275e7b | ||
|
|
6db0172a50 | ||
|
|
95ff5c98a8 | ||
|
|
e9f9f90137 | ||
|
|
0ebfecc60c | ||
|
|
afa29a0ed1 | ||
|
|
8d707dc815 | ||
|
|
5b509d147f | ||
|
|
bb6d909949 | ||
|
|
8513d63a3e | ||
|
|
d2f3345c7a | ||
|
|
aee3a0a281 | ||
|
|
4752fa6dd2 | ||
|
|
4e08cf3879 | ||
|
|
11bda8e76a | ||
|
|
e33172060f | ||
|
|
0dee6a9888 | ||
|
|
3d855dcbfc | ||
|
|
ed3b7f7971 | ||
|
|
c0a4fb16e2 | ||
|
|
688e7fff9c | ||
|
|
880e56e563 | ||
|
|
bf26d4ec95 | ||
|
|
d5df3de1d7 | ||
|
|
5d005211d4 | ||
|
|
cc08ac9236 | ||
|
|
09a6a340c9 | ||
|
|
2692104ccd | ||
|
|
b1fd539d25 | ||
|
|
33d48fe4f7 | ||
|
|
1ebc8e9362 | ||
|
|
8b5a5241f2 | ||
|
|
959b9ffbac | ||
|
|
c9cbe41f9e | ||
|
|
d62cbdc391 | ||
|
|
31bd8ce7ae | ||
|
|
c25db503e0 | ||
|
|
ac51e6aae3 | ||
|
|
72c9d616fd | ||
|
|
52f80293a2 | ||
|
|
a3a9edd41a | ||
|
|
f9cbff1eec | ||
|
|
b71482284f | ||
|
|
d90e3a816e | ||
|
|
869d875b5f | ||
|
|
fb1a360360 | ||
|
|
943765bd4d | ||
|
|
9280113350 | ||
|
|
627adb1755 | ||
|
|
ad421eae11 | ||
|
|
b0af9d535a | ||
|
|
5ab69dfb7f | ||
|
|
f1214e6ffd | ||
|
|
a09e029216 | ||
|
|
8782c70640 | ||
|
|
7099a36bdb | ||
|
|
7bd8b0c152 | ||
|
|
b1cbf54711 | ||
|
|
84f003d907 | ||
|
|
4257e696da | ||
|
|
c5c0d46ab8 | ||
|
|
b397240664 | ||
|
|
a31e27ee06 | ||
|
|
483d7b6e58 | ||
|
|
bfb6133871 | ||
|
|
917ff89d9d | ||
|
|
ef5762864f | ||
|
|
50586cdb42 | ||
|
|
82a0b8de0c | ||
|
|
6db800d6d2 | ||
|
|
b23bfc2f6e | ||
|
|
929279b35a | ||
|
|
795e4bad5f | ||
|
|
6e20b6034e | ||
|
|
6f34a27bd3 | ||
|
|
240a605977 | ||
|
|
4a6a471345 | ||
|
|
f1be4f50a3 | ||
|
|
8ef32e8081 | ||
|
|
71e02ef833 | ||
|
|
c70d7e475d | ||
|
|
0f31f05e61 | ||
|
|
7971a003cc | ||
|
|
49491b9b8c | ||
|
|
ea11f17954 | ||
|
|
ebd37b9e2f | ||
|
|
5de4fe3d23 | ||
|
|
eb47f1227a | ||
|
|
f84cde7d04 | ||
|
|
4d3eb2887c | ||
|
|
bc631612df | ||
|
|
5d8a211961 | ||
|
|
e62add6893 | ||
|
|
44fc429f64 | ||
|
|
ffe281f25d | ||
|
|
ba39ff433d | ||
|
|
795b8614ad | ||
|
|
745a2f1886 | ||
|
|
9bf58c89d4 | ||
|
|
ee62b9a4c7 | ||
|
|
e6125b893d | ||
|
|
83ed333fa5 | ||
|
|
39ed67d667 | ||
|
|
ac2e973cb0 | ||
|
|
94a0be3b05 | ||
|
|
124e8af649 | ||
|
|
f07ed68d82 | ||
|
|
8f39dbf6b1 | ||
|
|
6dde9ee6c4 | ||
|
|
dbe6fe442d | ||
|
|
1e2ac86ac6 | ||
|
|
83c5ba04cd | ||
|
|
b3f25c176b | ||
|
|
52cf6375eb | ||
|
|
82a3c37c16 | ||
|
|
d63df5a156 | ||
|
|
63ad3739fd | ||
|
|
7eddb16f2f | ||
|
|
7c4f4cacc9 | ||
|
|
dc6ad72b2d | ||
|
|
be729c87af | ||
|
|
4ee83d9da4 | ||
|
|
8b2f06442a | ||
|
|
21181f011f | ||
|
|
b4b099ecb1 | ||
|
|
166b30bb0a | ||
|
|
8eeb81f58e | ||
|
|
0b20a1eeaa | ||
|
|
d8cb8f7815 | ||
|
|
36f79593cf | ||
|
|
1014b25bf5 | ||
|
|
55dcfc1a85 | ||
|
|
9b6a628d51 | ||
|
|
8b5a42073d | ||
|
|
7991d40760 | ||
|
|
4203296414 | ||
|
|
93d319275f | ||
|
|
94d262263c | ||
|
|
ed90f21188 | ||
|
|
80e0231727 | ||
|
|
981197583a | ||
|
|
6f58344e7b | ||
|
|
07be844985 | ||
|
|
45262583e6 | ||
|
|
c113e38531 | ||
|
|
8771f311d7 | ||
|
|
fdec65e9bd | ||
|
|
f8b46dc647 | ||
|
|
847e47b5db | ||
|
|
227bad9e99 | ||
|
|
cb8168de41 | ||
|
|
c200a0c7ac | ||
|
|
81e80572d8 | ||
|
|
2aa6c83714 | ||
|
|
a22ea0b82b | ||
|
|
af64579eb2 | ||
|
|
f2705a611d | ||
|
|
990dcc9d5a | ||
|
|
c09237f0c3 | ||
|
|
571a3533fb | ||
|
|
6fcd9e1595 | ||
|
|
9caa42d257 | ||
|
|
18fdbbaabb | ||
|
|
7381c1c0cb | ||
|
|
2bdcae7209 | ||
|
|
fc694b90b6 | ||
|
|
945cd7ff8e | ||
|
|
3b32ca1e60 | ||
|
|
98611267d5 | ||
|
|
4d53002e5c | ||
|
|
f6b7b0d3d2 | ||
|
|
fbbd7ccf49 | ||
|
|
d41ce65a78 | ||
|
|
c477fd2071 | ||
|
|
7fab8395c8 | ||
|
|
7d72e0c046 | ||
|
|
9ce97be6a4 | ||
|
|
121b69b0c3 | ||
|
|
cb7cc2f276 | ||
|
|
d01849306e | ||
|
|
a4e87982a6 | ||
|
|
e1c3c80c0f | ||
|
|
fd6346ed59 | ||
|
|
2e4f7b5a8c | ||
|
|
d812d4e12e | ||
|
|
10b3f09e7e | ||
|
|
2d3776844c | ||
|
|
914a4574de | ||
|
|
2b3c84f21a | ||
|
|
f4eb1af8d0 | ||
|
|
c01fc332d2 | ||
|
|
b8fdbc3e94 | ||
|
|
3c7c55364b | ||
|
|
bff4355a1a | ||
|
|
9ea57a7df1 | ||
|
|
4c4171e7fb | ||
|
|
77ece044ad | ||
|
|
d633b36b23 | ||
|
|
2eda6601c0 | ||
|
|
6202393637 | ||
|
|
1b76044242 | ||
|
|
28648e27cf | ||
|
|
90921a74ea | ||
|
|
950b764ff1 | ||
|
|
15ba30bf8f | ||
|
|
c96654d50f | ||
|
|
b5f175d220 | ||
|
|
c535543922 | ||
|
|
9913030e6f | ||
|
|
e7f58fc7fa | ||
|
|
746ee50027 | ||
|
|
683c2da224 | ||
|
|
2671c83337 | ||
|
|
bd35c89c04 | ||
|
|
616baecafb | ||
|
|
bfe895c690 | ||
|
|
97aff2eb4c | ||
|
|
1c46ee2988 | ||
|
|
d0d4ee843d | ||
|
|
fd127da342 | ||
|
|
a8728336ca | ||
|
|
c0eb9bd1e9 | ||
|
|
c85896845f | ||
|
|
efe953d8cd | ||
|
|
b5593ef5b2 | ||
|
|
d08d2e00ee | ||
|
|
bc8908cca1 | ||
|
|
9109f0ccd5 | ||
|
|
30c2ef58cd | ||
|
|
23b295c7c1 | ||
|
|
db24ee4d37 | ||
|
|
e737a1edbd | ||
|
|
109d84302c | ||
|
|
e50974a86a | ||
|
|
ef491e1e96 | ||
|
|
f366a68159 | ||
|
|
0377faadd6 | ||
|
|
a5825373e1 | ||
|
|
fadfadd9e9 | ||
|
|
596b316d65 | ||
|
|
c1e24406d9 | ||
|
|
13dfb090b5 | ||
|
|
ddd1ff0101 | ||
|
|
b266a72726 | ||
|
|
255fae3a90 | ||
|
|
b4a60cfee2 | ||
|
|
233a2e6400 | ||
|
|
f31cb49e2a | ||
|
|
47fd12e7a4 | ||
|
|
d5eb679cf0 | ||
|
|
26d0e46367 | ||
|
|
146bfaa9de | ||
|
|
315051c14c | ||
|
|
3a7d770f6d | ||
|
|
2cd60af841 | ||
|
|
e2f5775b07 | ||
|
|
c27be733a9 | ||
|
|
54fda697ce | ||
|
|
04e0b56dd5 | ||
|
|
b71824c5e8 | ||
|
|
65293ea5e4 | ||
|
|
05c2f5bebd | ||
|
|
e8b2255be0 | ||
|
|
2c227d3475 | ||
|
|
958f7b535a | ||
|
|
9e34096139 | ||
|
|
12b5536e22 | ||
|
|
171a5b1793 | ||
|
|
b4fb82066b | ||
|
|
57145436ab | ||
|
|
cba69ca467 | ||
|
|
375fcab788 | ||
|
|
de65c02222 | ||
|
|
444b0beaca | ||
|
|
4c931278b8 | ||
|
|
3bdac794b3 | ||
|
|
67eb593164 | ||
|
|
fe230e7d30 | ||
|
|
2f0ce3ee37 | ||
|
|
992b8a6fb6 | ||
|
|
84e45caa6c | ||
|
|
68d9542816 | ||
|
|
c9c9c50d6c | ||
|
|
9f4e0ce920 | ||
|
|
388852d6b7 | ||
|
|
4e1f071951 | ||
|
|
8e47829905 | ||
|
|
84fe5004a9 | ||
|
|
28b51a9b46 | ||
|
|
07fc7af911 | ||
|
|
330405ae42 | ||
|
|
ffb26ba67f | ||
|
|
fc597abbc9 | ||
|
|
177f10f76d | ||
|
|
a7a7f83e3e | ||
|
|
b6fb44d6a5 | ||
|
|
996c2b55a4 | ||
|
|
21d9d31a27 | ||
|
|
00536cba3a | ||
|
|
641dde81e5 | ||
|
|
8177e98014 | ||
|
|
abfc6be84d | ||
|
|
1d15d582d9 | ||
|
|
5cd3c5fcc0 | ||
|
|
5e568d7dd8 | ||
|
|
c251bce44d | ||
|
|
1408dd48a1 | ||
|
|
a77734797a | ||
|
|
a119ae7efa | ||
|
|
7d284a7e18 | ||
|
|
4d65bd9f1b | ||
|
|
517511e5be | ||
|
|
2ef38fe06d | ||
|
|
b128d36121 | ||
|
|
082025f0b6 | ||
|
|
220b7f6d53 | ||
|
|
062723bf15 | ||
|
|
bcbbb64042 | ||
|
|
59230a0f9e | ||
|
|
18fb9c9de3 | ||
|
|
26e827e4dc | ||
|
|
2f828b1d39 | ||
|
|
5b22fcc2f5 | ||
|
|
4f36b00ec3 | ||
|
|
707c20513e | ||
|
|
cddd068887 | ||
|
|
cf6863b2c6 | ||
|
|
994d311ed3 | ||
|
|
1098f8cb1e | ||
|
|
1be1a2b8f7 | ||
|
|
07a6e40917 | ||
|
|
2c2057b5cb | ||
|
|
caf09e7498 | ||
|
|
9488468b67 | ||
|
|
d071bf8ade | ||
|
|
1626c01ff4 | ||
|
|
3dd6895662 | ||
|
|
0ab967b7a4 | ||
|
|
3b07946065 | ||
|
|
4c0a079d1e | ||
|
|
1878b39e21 | ||
|
|
7050111bf4 | ||
|
|
572f7d826a | ||
|
|
a155811678 | ||
|
|
54ccee3c0f | ||
|
|
88b7665b7f | ||
|
|
a66007f8cc | ||
|
|
d5f348c039 | ||
|
|
c0d1899ad3 | ||
|
|
ea14ef6314 | ||
|
|
75e6f1e39a | ||
|
|
f372fba1fd | ||
|
|
d494cee0da | ||
|
|
1b8380c5dc | ||
|
|
1ee2eb05eb | ||
|
|
9b82891abb | ||
|
|
64a28f891f | ||
|
|
c4944f31d6 | ||
|
|
6fd696546c | ||
|
|
e957dab993 | ||
|
|
831e7af9ed | ||
|
|
8ab779ffb9 | ||
|
|
506f9bf0e0 | ||
|
|
5151d751a3 | ||
|
|
bde9a97b17 | ||
|
|
cede823a33 | ||
|
|
dda2f6eb70 | ||
|
|
c54f2e08c2 | ||
|
|
2a2e025ef7 | ||
|
|
93ea4efb33 | ||
|
|
284301a659 | ||
|
|
8425dd9aa7 | ||
|
|
02bd8ed459 | ||
|
|
25a7cf18cf | ||
|
|
003929754d | ||
|
|
f6c0172257 | ||
|
|
75be3a3778 | ||
|
|
5cfb95e8ea | ||
|
|
8c2c4e233a | ||
|
|
f4ac4dee60 | ||
|
|
1b4053d959 | ||
|
|
8df5d972fc | ||
|
|
3a68d84376 | ||
|
|
865ea0ddd2 | ||
|
|
54dca31d66 | ||
|
|
17103570f1 | ||
|
|
b5d04a2031 | ||
|
|
86238af380 | ||
|
|
d10073a052 | ||
|
|
b99b0d4bf8 | ||
|
|
27b5b1bf10 | ||
|
|
bab9069dee | ||
|
|
52a3258814 | ||
|
|
da548f59d1 | ||
|
|
ecc500907c | ||
|
|
724dade1f6 | ||
|
|
c5dc869c03 | ||
|
|
273f7e4535 | ||
|
|
a58e060138 | ||
|
|
ef4d2fcc72 | ||
|
|
330c06d926 | ||
|
|
be9c36828a | ||
|
|
17f83135c5 | ||
|
|
543ba51d3c | ||
|
|
33df23fc8c | ||
|
|
3236d6b934 | ||
|
|
e0e7775367 | ||
|
|
198679583c | ||
|
|
6dae2a604f | ||
|
|
68905c6ae4 | ||
|
|
26630c4d64 | ||
|
|
d382f030f0 | ||
|
|
08fca87b2f | ||
|
|
33441a1c5c | ||
|
|
6d8346b13a | ||
|
|
d9b340ca45 | ||
|
|
ebbc52ee1f | ||
|
|
de5bee29ef | ||
|
|
25f843ec0b | ||
|
|
df600a9e14 | ||
|
|
156c25cea1 | ||
|
|
f7dd04e3de | ||
|
|
3036d86cfa | ||
|
|
3fccd52884 | ||
|
|
00640274fc | ||
|
|
a7e8fb98b3 | ||
|
|
bed6643437 | ||
|
|
f815e8511f | ||
|
|
6360fd42e7 | ||
|
|
62a9656888 | ||
|
|
ffb89c7e5b | ||
|
|
aa52735006 | ||
|
|
b65526d8ee | ||
|
|
ea8e2999ae | ||
|
|
0b5017f992 | ||
|
|
8bf1bdaa04 | ||
|
|
01eb3b1c94 | ||
|
|
3402c9f601 | ||
|
|
13c3518c5e | ||
|
|
821fad27dc | ||
|
|
50a34e2f4c | ||
|
|
2a19b2afbe | ||
|
|
dc92d010fb | ||
|
|
9cb27a616a | ||
|
|
518a0ca45b | ||
|
|
3526a0e3c5 | ||
|
|
6386f85258 | ||
|
|
e80106d8f8 | ||
|
|
1145cbc75c | ||
|
|
e669b81072 | ||
|
|
9d78da941b | ||
|
|
63d0f5e2c6 | ||
|
|
dae047eff1 | ||
|
|
8a2db8bced | ||
|
|
792fab20e6 | ||
|
|
f40c0f6bd3 | ||
|
|
ccf11b9861 | ||
|
|
1fcde5a17c | ||
|
|
88f543dd25 | ||
|
|
8b6f3f6022 | ||
|
|
294ef8045a | ||
|
|
1f7e4c886b | ||
|
|
63c047009f | ||
|
|
2c5f5004cc | ||
|
|
2fa5426cf5 | ||
|
|
428c777402 | ||
|
|
7e2c62c520 | ||
|
|
3d3b4f4a46 | ||
|
|
a543dcf166 | ||
|
|
5de54bb6bf | ||
|
|
d95401e614 | ||
|
|
2c835437e9 | ||
|
|
498e70ed2b | ||
|
|
fce5b500bf | ||
|
|
11def54adb | ||
|
|
9da9e73f7a | ||
|
|
f90cd49a6d | ||
|
|
6e72c07190 | ||
|
|
1997e1faeb | ||
|
|
b33b34bd71 | ||
|
|
6a9b739541 | ||
|
|
6cb0bdd1a4 | ||
|
|
b73ebb6f92 | ||
|
|
24a83260ca | ||
|
|
fc1c1b402b | ||
|
|
af462b3486 | ||
|
|
3e236996c8 | ||
|
|
c4e84483df | ||
|
|
e9532f2158 | ||
|
|
15fc8ab2e7 | ||
|
|
b39dbeb25e | ||
|
|
e181c21f85 | ||
|
|
db771bc2cc | ||
|
|
0695cfb3c0 | ||
|
|
ff3982efa4 | ||
|
|
40de7f5d54 | ||
|
|
18a848696f | ||
|
|
58de7375a2 | ||
|
|
b61109a269 | ||
|
|
164fd8f022 | ||
|
|
cafaa9ff22 | ||
|
|
34c98d1dcd | ||
|
|
ec015da795 | ||
|
|
a21a82c64e | ||
|
|
1aca669c62 | ||
|
|
39573ada54 | ||
|
|
bceb7c77d1 | ||
|
|
68fa771905 | ||
|
|
8a85968ec4 | ||
|
|
03b1a2dcff | ||
|
|
5cb231e427 | ||
|
|
e90f6764f5 | ||
|
|
d078055e40 | ||
|
|
98134eb612 | ||
|
|
b0f2dfc4fb | ||
|
|
195f07c09f | ||
|
|
15f87edc96 | ||
|
|
52caae8f05 | ||
|
|
99f7fc99da | ||
|
|
89fc98c6f6 | ||
|
|
0c2eb1caab | ||
|
|
303969bdbf | ||
|
|
8b7266a435 | ||
|
|
fc9282fff7 | ||
|
|
33fb79e0de | ||
|
|
b4418660df | ||
|
|
3560a22a77 | ||
|
|
818b466687 | ||
|
|
91d29c1e2c | ||
|
|
e06fe72131 | ||
|
|
7d715fdca0 | ||
|
|
3cfa6988ab | ||
|
|
1c707b2d37 | ||
|
|
2ed4c76abf | ||
|
|
94e91723f4 | ||
|
|
091bddbad8 | ||
|
|
b3f68c2638 | ||
|
|
00c94c8efd | ||
|
|
66b19677bf | ||
|
|
2d4170e0d7 | ||
|
|
7f8f38ddf1 | ||
|
|
2e1763f19a | ||
|
|
a5f8b3c164 | ||
|
|
a802087009 | ||
|
|
3b16c06f70 | ||
|
|
a979ae3ced | ||
|
|
02e0f40702 | ||
|
|
a4d7853bb2 | ||
|
|
ac463e0f65 | ||
|
|
c187aa46b7 | ||
|
|
e65fa9dcae | ||
|
|
fea230cfab | ||
|
|
90838c99fc | ||
|
|
8e96adeda9 | ||
|
|
3cdb0f840e | ||
|
|
b6ad6e0a85 | ||
|
|
e73a577452 | ||
|
|
16e8aa2447 | ||
|
|
1d6958a67e | ||
|
|
136ca282eb | ||
|
|
388fc2f7d9 | ||
|
|
72fdb7127a | ||
|
|
f67832644f | ||
|
|
2614f3261c | ||
|
|
e84204b49a | ||
|
|
86aaa725d5 | ||
|
|
dd583a176f | ||
|
|
5299d958f2 | ||
|
|
f0374cf9d9 | ||
|
|
6b6a0d7b4f | ||
|
|
4317892421 | ||
|
|
8052b2adfa | ||
|
|
d44927447a | ||
|
|
09e6c6422d | ||
|
|
df85ffb254 | ||
|
|
752ba4405c | ||
|
|
1f3a02b83d | ||
|
|
8e372f1e93 | ||
|
|
caeb84f58b | ||
|
|
759722bf7d | ||
|
|
cefdc95c9b | ||
|
|
3be7029078 | ||
|
|
3f16858a93 | ||
|
|
c61c6deaa8 | ||
|
|
90c8483df8 | ||
|
|
d70b189cec | ||
|
|
41a7fc4de5 | ||
|
|
db119d5230 | ||
|
|
c88245954d | ||
|
|
b1ab3834b6 | ||
|
|
34b7c1be81 | ||
|
|
082c77586f | ||
|
|
3b6fe7b548 | ||
|
|
da072e7621 | ||
|
|
4568404a70 | ||
|
|
43319853ef | ||
|
|
ce9f142621 | ||
|
|
8ef3add183 | ||
|
|
ef45696015 | ||
|
|
a8f8c2cd85 | ||
|
|
6d79a8e23a | ||
|
|
d65dc6ccac | ||
|
|
ccc9076a80 | ||
|
|
3c007cea34 | ||
|
|
bf29312ecf | ||
|
|
a53095b29e | ||
|
|
d5c9e6b054 | ||
|
|
d4f29bd2af | ||
|
|
a6661ac759 | ||
|
|
6aa83819d1 | ||
|
|
8ff4c8a4a5 | ||
|
|
1d77b8dae7 | ||
|
|
3e0062621b | ||
|
|
b20e220910 | ||
|
|
0259572ded | ||
|
|
a78866069b | ||
|
|
b8fc83577c | ||
|
|
c21969ab4e | ||
|
|
e02cfc4529 | ||
|
|
ac07c63631 | ||
|
|
1054bc995b | ||
|
|
3560ab6387 | ||
|
|
e50274b962 | ||
|
|
bb1773a1a1 | ||
|
|
941dc3b1b4 | ||
|
|
92d08d24ab | ||
|
|
7f6ffe0f73 | ||
|
|
66457ca0c7 | ||
|
|
3344f1fd88 | ||
|
|
c4b636f80a | ||
|
|
0ea0975bd4 | ||
|
|
534f3a7469 | ||
|
|
f3110ba018 | ||
|
|
e76cc81fe1 | ||
|
|
adcc3343ec | ||
|
|
645e114a1f | ||
|
|
65d86460cb | ||
|
|
aaccd10c2a | ||
|
|
a237bfd930 | ||
|
|
73f64d93b1 | ||
|
|
871db09447 | ||
|
|
8d79103392 | ||
|
|
fd765443e4 | ||
|
|
63967d1558 | ||
|
|
6b270885bf | ||
|
|
47937d6aaa | ||
|
|
7d2ba45620 | ||
|
|
b270d819a8 | ||
|
|
c50553fbf6 | ||
|
|
a541c863be | ||
|
|
254b482651 | ||
|
|
d3c2cd4215 | ||
|
|
4f7cc7dd6b | ||
|
|
21f1f4e503 | ||
|
|
47f2336673 | ||
|
|
5ae93bf6d0 | ||
|
|
caf5f10326 | ||
|
|
e68dbcf4ee | ||
|
|
a42e81cf8c | ||
|
|
98a8588c1b | ||
|
|
8630af7646 | ||
|
|
268c5302e8 | ||
|
|
d07d535993 | ||
|
|
a8a75f22b2 | ||
|
|
6143023502 | ||
|
|
ca6aa5d4aa | ||
|
|
bc028ed41f | ||
|
|
911d3a9188 | ||
|
|
b88e715fc5 | ||
|
|
a4dfa5f281 | ||
|
|
05fc1711b9 | ||
|
|
c16fbb5b47 | ||
|
|
7ca3e2b519 | ||
|
|
8c8a0bf8eb | ||
|
|
e85251d2e3 | ||
|
|
73e4827249 | ||
|
|
c37270ea08 | ||
|
|
8cc33b46bb | ||
|
|
700341f9cc | ||
|
|
ca85ad5995 | ||
|
|
1c8c36a224 | ||
|
|
25b814e796 | ||
|
|
e89566e04a | ||
|
|
e946f388c0 | ||
|
|
7616e8dab7 | ||
|
|
2dc4fef4d3 | ||
|
|
f2ca997195 | ||
|
|
7445004abf | ||
|
|
9b76abe2ed | ||
|
|
a97205c9fc | ||
|
|
bf3d069aad | ||
|
|
27ea74722c | ||
|
|
2525456d8b | ||
|
|
9fa32df3a6 | ||
|
|
5bdd5da13b | ||
|
|
b8756edd29 | ||
|
|
c93910e858 | ||
|
|
ad4226ace7 | ||
|
|
d71b3fe1bc | ||
|
|
07858eecac | ||
|
|
64ec6d0e58 | ||
|
|
cf722427ab | ||
|
|
1b7ff07efc | ||
|
|
87533f4417 | ||
|
|
9077c95cdd | ||
|
|
22acc5ae96 | ||
|
|
cf596a88ab | ||
|
|
7354fa3050 | ||
|
|
978497b287 | ||
|
|
a52d745250 | ||
|
|
aae71f8105 | ||
|
|
5419b4b732 | ||
|
|
caf5a8917c | ||
|
|
beec00dcb3 | ||
|
|
7565e809b0 | ||
|
|
5c9a646bc1 | ||
|
|
dd8ef288f7 | ||
|
|
db8d2953cb | ||
|
|
b75ca26db2 | ||
|
|
948a04122a | ||
|
|
b7c4562b85 | ||
|
|
6d0fea1983 | ||
|
|
1a158a919a | ||
|
|
9a83bd4267 | ||
|
|
7e3f516b04 | ||
|
|
afd888e14d | ||
|
|
76af6e975e | ||
|
|
2017df9ec6 | ||
|
|
aa1e83dc24 | ||
|
|
20996b153d | ||
|
|
b298e53fc4 | ||
|
|
7fb382bee0 | ||
|
|
2158772e3b | ||
|
|
333298e6c3 | ||
|
|
6e9deeba5b | ||
|
|
15951509a7 | ||
|
|
dd8b7e42d6 | ||
|
|
2655c86be3 | ||
|
|
c4c4d347cf | ||
|
|
a3f7239c1b | ||
|
|
f90c2ad74e | ||
|
|
2907cd173b | ||
|
|
779ee8294f | ||
|
|
a229c9e10e | ||
|
|
3f8d2e4242 | ||
|
|
c1a8b0c303 | ||
|
|
8e92c5b844 | ||
|
|
c366fbde22 | ||
|
|
890e866ae2 | ||
|
|
8eb11a8957 | ||
|
|
9cc65a386b | ||
|
|
e4d4af1587 | ||
|
|
b3aab5116a | ||
|
|
7227c76538 | ||
|
|
c2c59f4a9e | ||
|
|
eeede318fd | ||
|
|
3855bb4d56 | ||
|
|
89fa682721 | ||
|
|
cb701a7bbc | ||
|
|
407325b8ce | ||
|
|
161debb35a | ||
|
|
fd5385b127 | ||
|
|
e85e1410aa | ||
|
|
ac068f353a | ||
|
|
fe7c6d0d57 | ||
|
|
abf1e4a8ac | ||
|
|
f9dfb2d0c7 | ||
|
|
da23740f17 | ||
|
|
47f81251d7 | ||
|
|
7e01eca7f5 | ||
|
|
941b5c9e45 | ||
|
|
207c0d612d | ||
|
|
4035176d88 | ||
|
|
b9f9968f84 | ||
|
|
7a2dac8d5b | ||
|
|
2fb8ad146f | ||
|
|
2f4a7352d9 | ||
|
|
c3ff030542 | ||
|
|
aa05cd1449 | ||
|
|
31cd33f86c | ||
|
|
9abca204e3 | ||
|
|
e9760c2100 | ||
|
|
1c5bab63a5 | ||
|
|
0d80957639 | ||
|
|
6c73ddcaca | ||
|
|
1bb86fe4a8 | ||
|
|
6b7be7a82d | ||
|
|
37b25d8422 | ||
|
|
49edd1a6dc | ||
|
|
a338c6e60a | ||
|
|
f9805f3bc7 | ||
|
|
b24c4ea030 | ||
|
|
0cabac1eed | ||
|
|
7c08dbfbd2 | ||
|
|
c0ec74bbb7 | ||
|
|
ea834f6778 | ||
|
|
2271f32140 | ||
|
|
93906b9b17 | ||
|
|
40d84b7a82 | ||
|
|
d0ee569989 | ||
|
|
92d969b075 | ||
|
|
49b70d0efd | ||
|
|
26779ef1fb | ||
|
|
fd19af23a6 | ||
|
|
f798fef212 | ||
|
|
c74eab4fb5 | ||
|
|
6a0198639f | ||
|
|
8c870f2db8 | ||
|
|
73d287e7ee | ||
|
|
9eb3eea3f1 | ||
|
|
df769396b1 | ||
|
|
c2e47ca9dc | ||
|
|
b348159e0e | ||
|
|
45b62f0e77 | ||
|
|
1fb420c2fc | ||
|
|
b4f2bc1cb3 | ||
|
|
d6cc182da2 | ||
|
|
9d7baa86aa | ||
|
|
872cd90dc6 | ||
|
|
00ab816791 | ||
|
|
2a9e9962e8 | ||
|
|
ed25dd931e | ||
|
|
66031f1bc2 | ||
|
|
a902872880 | ||
|
|
4c2d440871 | ||
|
|
0da17de422 | ||
|
|
07025ae76b | ||
|
|
d99fe944f3 | ||
|
|
408b2a473e | ||
|
|
fc22e9e28a | ||
|
|
d1c44ab7b1 | ||
|
|
4ddac50d9b | ||
|
|
7208ad67f1 | ||
|
|
fffdeb1320 | ||
|
|
e8fa7d8812 | ||
|
|
dfdb92957e | ||
|
|
441069f04b | ||
|
|
201995eb90 | ||
|
|
5863319c0b | ||
|
|
2986d85b26 | ||
|
|
f312457f35 | ||
|
|
de501f5ba3 | ||
|
|
c5e5141b21 | ||
|
|
c08cf61d0c | ||
|
|
6728382141 | ||
|
|
7c3f104d1b | ||
|
|
ad6be11bbc | ||
|
|
f09faf6645 | ||
|
|
117a7762e1 | ||
|
|
3d47f494a8 | ||
|
|
e0ebdc9045 | ||
|
|
53f8e9328d | ||
|
|
687e0b563b | ||
|
|
6232cc7d49 | ||
|
|
2a6670a404 | ||
|
|
447efc7096 | ||
|
|
c47878202d | ||
|
|
349076bf34 | ||
|
|
1748bf2e2a | ||
|
|
d9a7730511 | ||
|
|
4a239cc217 | ||
|
|
ce2534c5b7 | ||
|
|
3083f5fd55 | ||
|
|
2045066b16 | ||
|
|
995177498f | ||
|
|
c00b7b62d6 | ||
|
|
93b772f197 | ||
|
|
7782f94daa | ||
|
|
7c97dc8004 | ||
|
|
34ce00e2d5 | ||
|
|
702658cca5 | ||
|
|
e472fe0276 | ||
|
|
fb1c381ab7 | ||
|
|
03c7998c11 | ||
|
|
35729fc36b | ||
|
|
f6ce603e45 | ||
|
|
81a75ca955 | ||
|
|
b57c9a51f8 | ||
|
|
df396966b0 | ||
|
|
b8897e0193 | ||
|
|
150e8112ea | ||
|
|
196f16b941 | ||
|
|
21cb7a4847 | ||
|
|
bb2dac7504 | ||
|
|
f5fd2f2be3 | ||
|
|
e6ea3879c3 | ||
|
|
c07895d418 | ||
|
|
58fb1cf4c3 | ||
|
|
52cc8cb8fc | ||
|
|
1b30dab8eb | ||
|
|
40df4a94a7 | ||
|
|
262d06f035 | ||
|
|
408ab99774 | ||
|
|
28cb21db13 | ||
|
|
7f37f4ca41 | ||
|
|
5f85258e84 | ||
|
|
cde1776a2d | ||
|
|
7f6303391a | ||
|
|
66c7806cfa | ||
|
|
2cdb6945ba | ||
|
|
ca45855ed7 | ||
|
|
07ed90ed11 | ||
|
|
c1b97b1b44 | ||
|
|
674019ea75 | ||
|
|
8f762484f2 | ||
|
|
4174991345 | ||
|
|
71064cc760 | ||
|
|
4c40c8ff30 | ||
|
|
d7211b130b | ||
|
|
e40d5a0a5d | ||
|
|
553fbf1a77 | ||
|
|
d3a4753b79 | ||
|
|
12cc0de571 | ||
|
|
cbd531e161 | ||
|
|
a8bbe02e21 | ||
|
|
6605d5ee63 | ||
|
|
df8bacd82e | ||
|
|
ee831da52d | ||
|
|
8f969374c7 | ||
|
|
5a788b04b5 | ||
|
|
b88a45aa79 | ||
|
|
3c20a056e6 | ||
|
|
82a57d34b8 | ||
|
|
c4d7076fe8 | ||
|
|
00b11157b2 | ||
|
|
b63bb1ac0c | ||
|
|
882d0ecba8 | ||
|
|
963f1a11eb | ||
|
|
ba8acb40ec | ||
|
|
1d4ea2164f | ||
|
|
0199243ce9 | ||
|
|
4c8b97afb3 | ||
|
|
3bac0225e5 |
@@ -7,10 +7,15 @@ indent_size = 2
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[Makefile]
|
[Makefile]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.tsx]
|
||||||
|
indent_style = tab
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ plugins:
|
|||||||
- lodash
|
- lodash
|
||||||
- jsdoc
|
- jsdoc
|
||||||
- node
|
- node
|
||||||
|
- react
|
||||||
extends: 'standard'
|
extends: 'standard'
|
||||||
parserOptions:
|
parserOptions:
|
||||||
sourceType: 'script'
|
sourceType: 'script'
|
||||||
|
ecmaFeatures:
|
||||||
|
jsx: true
|
||||||
settings:
|
settings:
|
||||||
jsdoc:
|
jsdoc:
|
||||||
additionalTagNames:
|
additionalTagNames:
|
||||||
@@ -286,7 +289,7 @@ rules:
|
|||||||
- error
|
- error
|
||||||
- anonymous: always
|
- anonymous: always
|
||||||
named: always
|
named: always
|
||||||
asyncArrow: never
|
asyncArrow: always
|
||||||
template-tag-spacing:
|
template-tag-spacing:
|
||||||
- error
|
- error
|
||||||
- always
|
- always
|
||||||
@@ -295,9 +298,6 @@ rules:
|
|||||||
|
|
||||||
# ECMAScript 6
|
# ECMAScript 6
|
||||||
|
|
||||||
arrow-body-style:
|
|
||||||
- error
|
|
||||||
- always
|
|
||||||
arrow-parens:
|
arrow-parens:
|
||||||
- error
|
- error
|
||||||
- always
|
- always
|
||||||
@@ -318,8 +318,6 @@ rules:
|
|||||||
- always
|
- always
|
||||||
prefer-const:
|
prefer-const:
|
||||||
- error
|
- error
|
||||||
prefer-reflect:
|
|
||||||
- error
|
|
||||||
prefer-spread:
|
prefer-spread:
|
||||||
- error
|
- error
|
||||||
prefer-numeric-literals:
|
prefer-numeric-literals:
|
||||||
@@ -444,3 +442,13 @@ rules:
|
|||||||
node/no-extraneous-import:
|
node/no-extraneous-import:
|
||||||
- error
|
- error
|
||||||
|
|
||||||
|
# React
|
||||||
|
|
||||||
|
react/jsx-uses-vars:
|
||||||
|
- error
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
files: ['*.jsx']
|
||||||
|
rules:
|
||||||
|
require-jsdoc:
|
||||||
|
- off
|
||||||
|
|||||||
15
.gitattributes
vendored
@@ -1,5 +1,8 @@
|
|||||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||||
*.js text eol=lf
|
*.js text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)
|
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)
|
||||||
*.css text eol=lf
|
*.css text eol=lf
|
||||||
*.scss text eol=lf
|
*.scss text eol=lf
|
||||||
@@ -11,7 +14,7 @@ Dockerfile* text
|
|||||||
etcher text
|
etcher text
|
||||||
.git* text
|
.git* text
|
||||||
*.html text
|
*.html text
|
||||||
*.json text
|
*.json text eol=lf
|
||||||
*.cpp text
|
*.cpp text
|
||||||
*.h text
|
*.h text
|
||||||
*.gyp text
|
*.gyp text
|
||||||
@@ -24,6 +27,9 @@ Makefile text
|
|||||||
*.yml text
|
*.yml text
|
||||||
*.patch text
|
*.patch text
|
||||||
*.txt text
|
*.txt text
|
||||||
|
*.tpl text
|
||||||
|
CODEOWNERS text
|
||||||
|
*.plist text
|
||||||
|
|
||||||
# Binary files (no line-ending conversions)
|
# Binary files (no line-ending conversions)
|
||||||
*.bz2 binary diff=hex
|
*.bz2 binary diff=hex
|
||||||
@@ -44,5 +50,12 @@ Makefile text
|
|||||||
*.bin binary diff=hex
|
*.bin binary diff=hex
|
||||||
*.dmg binary diff=hex
|
*.dmg binary diff=hex
|
||||||
*.rpi-sdcard binary diff=hex
|
*.rpi-sdcard binary diff=hex
|
||||||
|
*.wic binary diff=hex
|
||||||
*.foo binary diff=hex
|
*.foo binary diff=hex
|
||||||
|
*.eot binary diff=hex
|
||||||
|
*.otf binary diff=hex
|
||||||
|
*.woff binary diff=hex
|
||||||
|
*.woff2 binary diff=hex
|
||||||
|
*.ttf binary diff=hex
|
||||||
xz-without-extension binary diff=hex
|
xz-without-extension binary diff=hex
|
||||||
|
wmic-output.txt binary diff=hex
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,6 +1,11 @@
|
|||||||
- **Etcher version:**
|
- **Etcher version:**
|
||||||
- **Operating system and architecture:**
|
- **Operating system and architecture:**
|
||||||
- **Image flashed:**
|
- **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?**
|
- **Do you see any meaningful error information in the DevTools?**
|
||||||
|
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
|
||||||
|
|
||||||
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Alt+I` if you're on Mac OS. -->
|
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
|
||||||
|
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
|
||||||
|
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->
|
||||||
|
|||||||
8
.gitignore
vendored
@@ -43,3 +43,11 @@ node_modules
|
|||||||
*.cer
|
*.cer
|
||||||
*.crt
|
*.crt
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
# OSX files
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# VSCode files
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
|||||||
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "scripts/resin"]
|
||||||
|
path = scripts/resin
|
||||||
|
url = https://github.com/balena-io/scripts.git
|
||||||
|
branch = master
|
||||||
@@ -1,64 +1,73 @@
|
|||||||
{
|
{
|
||||||
"node-cli": {
|
|
||||||
"node": "6.1.0",
|
|
||||||
"main": "lib/cli/etcher.js",
|
|
||||||
"dependencies": {
|
|
||||||
"linux": [
|
|
||||||
"libudev-dev",
|
|
||||||
"libusb-1.0-0-dev"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"electron": {
|
"electron": {
|
||||||
|
"npm_version": "6.14.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"linux": [
|
"linux": [
|
||||||
"libudev-dev",
|
"libudev-dev",
|
||||||
"libusb-1.0-0-dev",
|
"libusb-1.0-0-dev",
|
||||||
"libyaml-dev"
|
"libyaml-dev",
|
||||||
|
"libgtk-3-0",
|
||||||
|
"libatk-bridge2.0-0",
|
||||||
|
"libdbus-1-3",
|
||||||
|
"libgbm1",
|
||||||
|
"libc6"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"builder": {
|
"builder": {
|
||||||
"appId": "io.resin.etcher",
|
"appId": "io.balena.etcher",
|
||||||
"copyright": "Copyright 2016-2018 Resinio Ltd",
|
"copyright": "Copyright 2016-2021 Balena Ltd",
|
||||||
"productName": "Etcher",
|
"productName": "balenaEtcher",
|
||||||
"nodeGypRebuild": true,
|
"nodeGypRebuild": false,
|
||||||
|
"afterPack": "./afterPack.js",
|
||||||
|
"asar": false,
|
||||||
"files": [
|
"files": [
|
||||||
"!lib/gui/app",
|
"generated",
|
||||||
"lib/gui/app/index.html",
|
"lib/shared/catalina-sudo/sudo-askpass.osascript.js"
|
||||||
"generated"
|
|
||||||
],
|
],
|
||||||
|
"afterSign": "./afterSignHook.js",
|
||||||
"mac": {
|
"mac": {
|
||||||
"category": "public.app-category.developer-tools"
|
"category": "public.app-category.developer-tools",
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"entitlements": "entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "entitlements.mac.plist",
|
||||||
|
"artifactName": "${productName}-${version}.${ext}"
|
||||||
},
|
},
|
||||||
"dmg": {
|
"dmg": {
|
||||||
"iconSize": 110,
|
"iconSize": 110,
|
||||||
"contents": [
|
"contents": [
|
||||||
{
|
{
|
||||||
"x": 140,
|
"x": 140,
|
||||||
"y": 225
|
"y": 245
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 415,
|
"x": 415,
|
||||||
"y": 225,
|
"y": 245,
|
||||||
"type": "link",
|
"type": "link",
|
||||||
"path": "/Applications"
|
"path": "/Applications"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"window": {
|
"window": {
|
||||||
"width": 540,
|
"width": 544,
|
||||||
"height": 405
|
"height": 407
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"category": "Utility",
|
"category": "Utility",
|
||||||
"packageCategory": "utils",
|
"packageCategory": "utils",
|
||||||
"synopsis": "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."
|
"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": {
|
"deb": {
|
||||||
|
"compression": "bzip2",
|
||||||
"priority": "optional",
|
"priority": "optional",
|
||||||
"depends": [
|
"depends": [
|
||||||
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
|
"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
|
|
||||||
|
|
||||||
69
.travis.yml
@@ -1,69 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
sudo: false
|
|
||||||
node_js:
|
|
||||||
- "6.10.3"
|
|
||||||
|
|
||||||
# Remove wine from cache
|
|
||||||
before_cache:
|
|
||||||
- rm -rf $HOME/.cache/electron-builder/wine
|
|
||||||
|
|
||||||
cache:
|
|
||||||
ccache: true
|
|
||||||
directories:
|
|
||||||
- $HOME/.cache/electron
|
|
||||||
- $HOME/.cache/electron-builder
|
|
||||||
- $HOME/.npm/_prebuilds
|
|
||||||
- $HOME/Library/Caches/electron
|
|
||||||
- $HOME/Library/Caches/electron-builder
|
|
||||||
- $HOME/.pkg-cache
|
|
||||||
- node_modules
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
sources:
|
|
||||||
- ubuntu-toolchain-r-test
|
|
||||||
packages:
|
|
||||||
- libstdc++-6-dev
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- CCACHE_TEMPDIR=/tmp/.ccache-temp
|
|
||||||
- CCACHE_COMPRESS=1
|
|
||||||
- CC="clang"
|
|
||||||
- CXX="clang++"
|
|
||||||
- HOMEBREW_NO_AUTO_UPDATE=1
|
|
||||||
matrix:
|
|
||||||
- TARGET_ARCH=x64
|
|
||||||
- TARGET_ARCH=x86
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
|
||||||
exclude:
|
|
||||||
- os: osx
|
|
||||||
env: TARGET_ARCH=x86
|
|
||||||
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- export HOST_OS="$TRAVIS_OS_NAME";
|
|
||||||
|
|
||||||
install:
|
|
||||||
- ./scripts/ci/install.sh -o $HOST_OS -r $TARGET_ARCH
|
|
||||||
|
|
||||||
script:
|
|
||||||
- ./scripts/ci/test.sh -o $HOST_OS -r $TARGET_ARCH
|
|
||||||
- ./scripts/ci/build-installers.sh -o $HOST_OS -r $TARGET_ARCH
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
provider: script
|
|
||||||
skip_cleanup: true
|
|
||||||
script: scripts/ci/deploy.sh -o $HOST_OS -r $TARGET_ARCH
|
|
||||||
on:
|
|
||||||
branch: master
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
11669
.versionbot/CHANGELOG.yml
Normal file
1628
CHANGELOG.md
46
FAQ.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
## Why is my drive not bootable?
|
||||||
|
|
||||||
|
Etcher copies images to drives byte by byte, without doing any transformation to the final device, which means images that require special treatment to be made bootable, like Windows images, will not work out of the box. In these cases, the general advice is to use software specific to those kind of images, usually available from the image publishers themselves. You can find more information [here](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#why-is-my-drive-not-bootable).
|
||||||
|
|
||||||
|
## How can I configure persistent storage?
|
||||||
|
|
||||||
|
Some programs, usually oriented at making GNU/Linux live USB drives, include an option to set persistent storage. This is currently not supported by Etcher, so if you require this functionality, we advise to fallback to [UNetbootin](https://unetbootin.github.io/).
|
||||||
|
|
||||||
|
## How do I flash Ubuntu ISOs
|
||||||
|
|
||||||
|
Ubuntu images (and potentially some other related GNU/Linux distributions) have a peculiar format that allows the image to boot without any further modification from both CDs and USB drives.
|
||||||
|
A consequence of this enhancement is that some programs, like parted get confused about the drive's format and partition table, printing warnings such as:
|
||||||
|
|
||||||
|
> /dev/xxx contains GPT signatures, indicating that it has a GPT table. However, it does not have a valid fake msdos partition table, as it should. Perhaps it was corrupted -- possibly by a program that doesn't understand GPT partition tables. Or perhaps you deleted the GPT table, and are now using an msdos partition table. Is this a GPT partition table? Both the primary and backup GPT tables are corrupt. Try making a fresh table, and using Parted's rescue feature to recover partitions.
|
||||||
|
|
||||||
|
> Warning: The driver descriptor says the physical block size is 2048 bytes, but Linux says it is 512 bytes.
|
||||||
|
|
||||||
|
All these warnings are safe to ignore, and your drive should be able to boot without any problems.
|
||||||
|
Refer to [the following message from Ubuntu's mailing list](https://lists.ubuntu.com/archives/ubuntu-devel/2011-June/033495.html) if you want to learn more.
|
||||||
|
|
||||||
|
## How do I run Etcher on Wayland?
|
||||||
|
|
||||||
|
The XWayland Server provides backwards compatibility to run any X client on Wayland, including Etcher.
|
||||||
|
This usually works out of the box on mainstream GNU/Linux distributions that properly support Wayland. If it doesn't, make sure the xwayland.so module is being loaded by declaring it in your [weston.ini](http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html):
|
||||||
|
|
||||||
|
```
|
||||||
|
[core]
|
||||||
|
modules=xwayland.so
|
||||||
|
```
|
||||||
|
|
||||||
|
## What are the runtime GNU/LINUX dependencies?
|
||||||
|
|
||||||
|
[This entry](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#runtime-gnulinux-dependencies) aims to provide an up to date list of runtime dependencies needed to run Etcher on a GNU/Linux system.
|
||||||
|
|
||||||
|
## How can I recover the broken drive?
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
||||||
529
Makefile
@@ -2,27 +2,22 @@
|
|||||||
# Build configuration
|
# Build configuration
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
# A non-existing target to force rules to rebuild
|
RESIN_SCRIPTS ?= ./scripts/resin
|
||||||
# See https://stackoverflow.com/a/816416
|
export NPM_VERSION ?= 6.14.8
|
||||||
.FORCE:
|
S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||||
|
|
||||||
# This directory will be completely deleted by the `clean` rule
|
# This directory will be completely deleted by the `clean` rule
|
||||||
BUILD_DIRECTORY ?= dist
|
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_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
|
||||||
|
|
||||||
# See https://github.com/electron/spectron/issues/127
|
$(BUILD_DIRECTORY):
|
||||||
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
|
mkdir $@
|
||||||
|
|
||||||
|
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
|
||||||
|
mkdir $@
|
||||||
|
|
||||||
# See https://stackoverflow.com/a/13468229/1641422
|
|
||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
# Operating system and architecture detection
|
# Operating system and architecture detection
|
||||||
@@ -71,14 +66,17 @@ else
|
|||||||
ifeq ($(shell uname -m),x86_64)
|
ifeq ($(shell uname -m),x86_64)
|
||||||
HOST_ARCH = x64
|
HOST_ARCH = x64
|
||||||
endif
|
endif
|
||||||
|
ifeq ($(shell uname -m),arm64)
|
||||||
|
HOST_ARCH = aarch64
|
||||||
|
endif
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifndef PLATFORM
|
ifndef PLATFORM
|
||||||
$(error We couldn't detect your host platform)
|
$(error We could not detect your host platform)
|
||||||
endif
|
endif
|
||||||
ifndef HOST_ARCH
|
ifndef HOST_ARCH
|
||||||
$(error We couldn't detect your host architecture)
|
$(error We could not detect your host architecture)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Default to host architecture. You can override by doing:
|
# Default to host architecture. You can override by doing:
|
||||||
@@ -87,289 +85,29 @@ endif
|
|||||||
#
|
#
|
||||||
TARGET_ARCH ?= $(HOST_ARCH)
|
TARGET_ARCH ?= $(HOST_ARCH)
|
||||||
|
|
||||||
# Support x86 builds from x64 in GNU/Linux
|
|
||||||
# See https://github.com/addaleax/lzma-native/issues/27
|
|
||||||
ifeq ($(PLATFORM),linux)
|
|
||||||
ifneq ($(HOST_ARCH),$(TARGET_ARCH))
|
|
||||||
ifeq ($(TARGET_ARCH),x86)
|
|
||||||
export CFLAGS += -m32
|
|
||||||
else
|
|
||||||
$(error Can't build $(TARGET_ARCH) binaries on a $(HOST_ARCH) host)
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
# Application configuration
|
# Electron
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
|
electron-develop:
|
||||||
|
git submodule update --init && \
|
||||||
|
npm ci && \
|
||||||
|
npm run webpack
|
||||||
|
|
||||||
ELECTRON_VERSION = $(shell jq -r '.devDependencies["electron"]' package.json)
|
electron-test:
|
||||||
NODE_VERSION = 6.1.0
|
$(RESIN_SCRIPTS)/electron/test.sh \
|
||||||
COMPANY_NAME = Resinio Ltd
|
-b $(shell pwd) \
|
||||||
APPLICATION_NAME = $(shell jq -r '.displayName' package.json)
|
-s $(PLATFORM)
|
||||||
APPLICATION_DESCRIPTION = $(shell jq -r '.description' package.json)
|
|
||||||
APPLICATION_COPYRIGHT = $(shell cat electron-builder.yml | shyaml get-value copyright)
|
|
||||||
|
|
||||||
BINTRAY_ORGANIZATION = resin-io
|
|
||||||
BINTRAY_REPOSITORY_DEBIAN = debian
|
|
||||||
BINTRAY_REPOSITORY_REDHAT = redhat
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Extra variables
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
TARGET_ARCH_DEBIAN = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t debian)
|
|
||||||
TARGET_ARCH_REDHAT = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t redhat)
|
|
||||||
TARGET_ARCH_APPIMAGE = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t appimage)
|
|
||||||
TARGET_ARCH_ELECTRON_BUILDER = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t electron-builder)
|
|
||||||
PLATFORM_PKG = $(shell ./scripts/build/platform-convert.sh -r $(PLATFORM) -t pkg)
|
|
||||||
ENTRY_POINT_CLI = lib/cli/etcher.js
|
|
||||||
ETCHER_CLI_BINARY = $(APPLICATION_NAME_LOWERCASE)
|
|
||||||
ifeq ($(PLATFORM),win32)
|
|
||||||
ETCHER_CLI_BINARY = $(APPLICATION_NAME_LOWERCASE).exe
|
|
||||||
endif
|
|
||||||
|
|
||||||
APPLICATION_NAME_LOWERCASE = $(shell echo $(APPLICATION_NAME) | tr A-Z a-z)
|
|
||||||
APPLICATION_VERSION_DEBIAN = $(shell echo $(APPLICATION_VERSION) | tr "-" "~")
|
|
||||||
APPLICATION_VERSION_REDHAT = $(shell echo $(APPLICATION_VERSION) | tr "-" "~")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Release type
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Add the current commit to the version if release type is "snapshot"
|
|
||||||
RELEASE_TYPE ?= snapshot
|
|
||||||
PACKAGE_JSON_VERSION = $(shell jq -r '.version' package.json)
|
|
||||||
ifeq ($(RELEASE_TYPE),production)
|
|
||||||
APPLICATION_VERSION = $(PACKAGE_JSON_VERSION)
|
|
||||||
S3_BUCKET = resin-production-downloads
|
|
||||||
BINTRAY_COMPONENT = $(APPLICATION_NAME_LOWERCASE)
|
|
||||||
endif
|
|
||||||
ifeq ($(RELEASE_TYPE),snapshot)
|
|
||||||
CURRENT_COMMIT_HASH = $(shell git log -1 --format="%h")
|
|
||||||
APPLICATION_VERSION = $(PACKAGE_JSON_VERSION)+$(CURRENT_COMMIT_HASH)
|
|
||||||
S3_BUCKET = resin-nightly-downloads
|
|
||||||
BINTRAY_COMPONENT = $(APPLICATION_NAME_LOWERCASE)-devel
|
|
||||||
endif
|
|
||||||
ifndef APPLICATION_VERSION
|
|
||||||
$(error Invalid release type: $(RELEASE_TYPE))
|
|
||||||
endif
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Code signing
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
ifeq ($(PLATFORM),darwin)
|
|
||||||
ifndef CSC_NAME
|
|
||||||
$(warning No code-sign identity found (CSC_NAME is not set))
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifeq ($(PLATFORM),win32)
|
|
||||||
ifndef CSC_LINK
|
|
||||||
$(warning No code-sign certificate found (CSC_LINK is not set))
|
|
||||||
ifndef CSC_KEY_PASSWORD
|
|
||||||
$(warning No code-sign certificate password found (CSC_KEY_PASSWORD is not set))
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Electron Builder
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
ELECTRON_BUILDER_OPTIONS = --$(TARGET_ARCH_ELECTRON_BUILDER)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Analytics
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
ifndef ANALYTICS_SENTRY_TOKEN
|
|
||||||
$(warning No Sentry token found (ANALYTICS_SENTRY_TOKEN is not set))
|
|
||||||
else
|
|
||||||
ELECTRON_BUILDER_OPTIONS += --extraMetadata.analytics.sentry.token=$(ANALYTICS_SENTRY_TOKEN)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifndef ANALYTICS_MIXPANEL_TOKEN
|
|
||||||
$(warning No Mixpanel token found (ANALYTICS_MIXPANEL_TOKEN is not set))
|
|
||||||
else
|
|
||||||
ELECTRON_BUILDER_OPTIONS += --extraMetadata.analytics.mixpanel.token=$(ANALYTICS_MIXPANEL_TOKEN)
|
|
||||||
endif
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Rules
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
# See http://stackoverflow.com/a/12528721
|
|
||||||
# Note that the blank line before 'endef' is actually important - don't delete it
|
|
||||||
define execute-command
|
|
||||||
$(1)
|
|
||||||
|
|
||||||
endef
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY):
|
|
||||||
mkdir $@
|
|
||||||
|
|
||||||
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
|
|
||||||
mkdir $@
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# CLI
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)-app: \
|
|
||||||
package.json npm-shrinkwrap.json \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
mkdir -p $@
|
|
||||||
./scripts/build/dependencies-npm.sh -p \
|
|
||||||
-r "$(TARGET_ARCH)" \
|
|
||||||
-v "$(NODE_VERSION)" \
|
|
||||||
-x $@ \
|
|
||||||
-t node \
|
|
||||||
-s "$(PLATFORM)"
|
|
||||||
patch --directory=$@ --force --strip=1 --ignore-whitespace < patches/lzma-native-index-static-addon-require.patch
|
|
||||||
cp -r lib $@
|
|
||||||
cp package.json $@
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH): \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)-app \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
mkdir $@
|
|
||||||
cd $< && pkg --output ../../$@/$(ETCHER_CLI_BINARY) -t node6-$(PLATFORM_PKG)-$(TARGET_ARCH) $(ENTRY_POINT_CLI)
|
|
||||||
./scripts/build/dependencies-npm-extract-addons.sh \
|
|
||||||
-d $</node_modules \
|
|
||||||
-o $@/node_modules
|
|
||||||
# pkg currently has a bug where darwin executables
|
|
||||||
# can't be code-signed
|
|
||||||
# See https://github.com/zeit/pkg/issues/128
|
|
||||||
# ifeq ($(PLATFORM),darwin)
|
|
||||||
# ifdef CSC_NAME
|
|
||||||
# ./scripts/build/electron-sign-file-darwin.sh -f $@/$(ETCHER_CLI_BINARY) -i "$(CSC_NAME)"
|
|
||||||
# endif
|
|
||||||
# endif
|
|
||||||
|
|
||||||
# pkg currently has a bug where Windows executables
|
|
||||||
# can't be branded
|
|
||||||
# See https://github.com/zeit/pkg/issues/149
|
|
||||||
# ifeq ($(PLATFORM),win32)
|
|
||||||
# ./scripts/build/electron-brand-exe.sh \
|
|
||||||
# -f $@/$(ETCHER_CLI_BINARY) \
|
|
||||||
# -n $(APPLICATION_NAME) \
|
|
||||||
# -d "$(APPLICATION_DESCRIPTION)" \
|
|
||||||
# -v "$(APPLICATION_VERSION)" \
|
|
||||||
# -c "$(APPLICATION_COPYRIGHT)" \
|
|
||||||
# -m "$(COMPANY_NAME)" \
|
|
||||||
# -i assets/icon.ico \
|
|
||||||
# -w $(BUILD_TEMPORARY_DIRECTORY)
|
|
||||||
# endif
|
|
||||||
|
|
||||||
ifeq ($(PLATFORM),win32)
|
|
||||||
ifdef CSC_LINK
|
|
||||||
ifdef CSC_KEY_PASSWORD
|
|
||||||
./scripts/build/electron-sign-exe-win32.sh -f $@/$(ETCHER_CLI_BINARY) \
|
|
||||||
-d "$(APPLICATION_NAME) - $(APPLICATION_VERSION)" \
|
|
||||||
-c $(CSC_LINK) \
|
|
||||||
-p $(CSC_KEY_PASSWORD)
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).zip: \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
|
|
||||||
./scripts/build/zip-file.sh -f $< -s $(PLATFORM) -o $@
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz: \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
|
|
||||||
./scripts/build/tar-gz-file.sh -f $< -o $@
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# GUI
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
|
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
|
||||||
tiffutil -cathidpicheck $^ -out $@
|
tiffutil -cathidpicheck $^ -out $@
|
||||||
|
|
||||||
build/js/gui.js: .FORCE
|
electron-build: assets/dmg/background.tiff | $(BUILD_TEMPORARY_DIRECTORY)
|
||||||
webpack
|
$(RESIN_SCRIPTS)/electron/build.sh \
|
||||||
|
-b $(shell pwd) \
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg: assets/dmg/background.tiff build/js/gui.js \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
TARGET_ARCH=$(TARGET_ARCH) build --mac dmg $(ELECTRON_BUILDER_OPTIONS) \
|
|
||||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
|
||||||
--extraMetadata.packageType=dmg
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip: assets/dmg/background.tiff build/js/gui.js \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
TARGET_ARCH=$(TARGET_ARCH) build --mac zip $(ELECTRON_BUILDER_OPTIONS) \
|
|
||||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
|
||||||
--extraMetadata.packageType=zip
|
|
||||||
|
|
||||||
APPLICATION_NAME_ELECTRON = $(APPLICATION_NAME_LOWERCASE)-electron
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm: build/js/gui.js \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
build --linux rpm $(ELECTRON_BUILDER_OPTIONS) \
|
|
||||||
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
|
|
||||||
--extraMetadata.version=$(APPLICATION_VERSION_REDHAT) \
|
|
||||||
--extraMetadata.packageType=rpm
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb: build/js/gui.js \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
build --linux deb $(ELECTRON_BUILDER_OPTIONS) \
|
|
||||||
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
|
|
||||||
--extraMetadata.version=$(APPLICATION_VERSION_DEBIAN) \
|
|
||||||
--extraMetadata.packageType=deb
|
|
||||||
|
|
||||||
ifeq ($(TARGET_ARCH),x64)
|
|
||||||
ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY = linux-unpacked
|
|
||||||
else
|
|
||||||
ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY = linux-$(TARGET_ARCH_ELECTRON_BUILDER)-unpacked
|
|
||||||
endif
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY)/$(APPLICATION_NAME_ELECTRON): build/js/gui.js | $(BUILD_DIRECTORY)
|
|
||||||
build --dir --linux $(ELECTRON_BUILDER_OPTIONS) \
|
|
||||||
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
|
|
||||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
|
||||||
--extraMetadata.packageType=AppImage
|
|
||||||
touch $@
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM).AppDir: \
|
|
||||||
$(BUILD_DIRECTORY)/$(ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY)/$(APPLICATION_NAME_ELECTRON) \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
./scripts/build/electron-create-appdir.sh \
|
|
||||||
-n $(APPLICATION_NAME) \
|
|
||||||
-d "$(APPLICATION_DESCRIPTION)" \
|
|
||||||
-p $(dir $<) \
|
|
||||||
-r $(TARGET_ARCH) \
|
-r $(TARGET_ARCH) \
|
||||||
-b $(APPLICATION_NAME_ELECTRON) \
|
-s $(PLATFORM) \
|
||||||
-i assets/icon.png \
|
-v production \
|
||||||
-o $@
|
-n $(BUILD_TEMPORARY_DIRECTORY)/npm
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET_ARCH_APPIMAGE).AppImage: \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM).AppDir \
|
|
||||||
| $(BUILD_DIRECTORY) $(BUILD_TEMPORARY_DIRECTORY)
|
|
||||||
./scripts/build/electron-create-appimage-linux.sh \
|
|
||||||
-d $< \
|
|
||||||
-r $(TARGET_ARCH) \
|
|
||||||
-w $(BUILD_TEMPORARY_DIRECTORY) \
|
|
||||||
-o $@
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip: \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET_ARCH_APPIMAGE).AppImage \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
./scripts/build/zip-file.sh -f $< -s $(PLATFORM) -o $@
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe: build/js/gui.js \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
TARGET_ARCH=$(TARGET_ARCH) build --win portable $(ELECTRON_BUILDER_OPTIONS) \
|
|
||||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
|
||||||
--extraMetadata.packageType=portable
|
|
||||||
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe: build/js/gui.js \
|
|
||||||
| $(BUILD_DIRECTORY)
|
|
||||||
TARGET_ARCH=$(TARGET_ARCH) build --win nsis $(ELECTRON_BUILDER_OPTIONS) \
|
|
||||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
|
||||||
--extraMetadata.packageType=nsis
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
# Phony targets
|
# Phony targets
|
||||||
@@ -379,229 +117,36 @@ TARGETS = \
|
|||||||
help \
|
help \
|
||||||
info \
|
info \
|
||||||
lint \
|
lint \
|
||||||
lint-js \
|
|
||||||
lint-sass \
|
|
||||||
lint-cpp \
|
|
||||||
lint-html \
|
|
||||||
lint-spell \
|
|
||||||
test-spectron \
|
|
||||||
test-gui \
|
|
||||||
test-sdk \
|
|
||||||
test-cli \
|
|
||||||
test \
|
test \
|
||||||
sanity-checks \
|
|
||||||
clean \
|
clean \
|
||||||
distclean \
|
distclean \
|
||||||
changelog \
|
electron-develop \
|
||||||
webpack \
|
electron-test \
|
||||||
package-electron \
|
electron-build
|
||||||
package-cli \
|
|
||||||
cli-develop \
|
|
||||||
installers-all \
|
|
||||||
publish-all \
|
|
||||||
electron-develop
|
|
||||||
|
|
||||||
changelog:
|
|
||||||
versionist
|
|
||||||
|
|
||||||
webpack: build/js/gui.js
|
|
||||||
|
|
||||||
package-electron:
|
|
||||||
TARGET_ARCH=$(TARGET_ARCH) build --dir $(ELECTRON_BUILDER_OPTIONS)
|
|
||||||
|
|
||||||
package-cli: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
|
|
||||||
|
|
||||||
ifeq ($(PLATFORM),darwin)
|
|
||||||
electron-installer-app-zip: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip
|
|
||||||
electron-installer-dmg: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg
|
|
||||||
cli-installer-tar-gz: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
|
||||||
TARGETS += \
|
|
||||||
electron-installer-dmg \
|
|
||||||
electron-installer-app-zip \
|
|
||||||
cli-installer-tar-gz
|
|
||||||
PUBLISH_AWS_S3 += \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifeq ($(PLATFORM),linux)
|
|
||||||
electron-installer-appimage: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip
|
|
||||||
electron-installer-debian: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
|
|
||||||
electron-installer-redhat: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm
|
|
||||||
cli-installer-tar-gz: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
|
||||||
TARGETS += \
|
|
||||||
electron-installer-appimage \
|
|
||||||
electron-installer-debian \
|
|
||||||
electron-installer-redhat \
|
|
||||||
cli-installer-tar-gz
|
|
||||||
PUBLISH_AWS_S3 += \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
|
||||||
PUBLISH_BINTRAY_DEBIAN += \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
|
|
||||||
PUBLISH_BINTRAY_REDHAT += \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifeq ($(PLATFORM),win32)
|
|
||||||
electron-installer-portable: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe
|
|
||||||
electron-installer-nsis: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe
|
|
||||||
cli-installer-zip: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).zip
|
|
||||||
TARGETS += \
|
|
||||||
electron-installer-portable \
|
|
||||||
electron-installer-nsis \
|
|
||||||
cli-installer-zip
|
|
||||||
PUBLISH_AWS_S3 += \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe \
|
|
||||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).zip
|
|
||||||
endif
|
|
||||||
|
|
||||||
installers-all: $(PUBLISH_AWS_S3) $(PUBLISH_BINTRAY_DEBIAN) $(PUBLISH_BINTRAY_REDHAT)
|
|
||||||
|
|
||||||
ifdef PUBLISH_AWS_S3
|
|
||||||
publish-aws-s3: $(PUBLISH_AWS_S3)
|
|
||||||
ifeq ($(RELEASE_TYPE),production)
|
|
||||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/aws-s3.sh \
|
|
||||||
-f $(publishable) \
|
|
||||||
-b $(S3_BUCKET) \
|
|
||||||
-v $(APPLICATION_VERSION) \
|
|
||||||
-p $(APPLICATION_NAME_LOWERCASE)))
|
|
||||||
endif
|
|
||||||
ifeq ($(RELEASE_TYPE),snapshot)
|
|
||||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/aws-s3.sh \
|
|
||||||
-f $(publishable) \
|
|
||||||
-b $(S3_BUCKET) \
|
|
||||||
-v $(APPLICATION_VERSION) \
|
|
||||||
-p $(APPLICATION_NAME_LOWERCASE) \
|
|
||||||
-k $(shell date +"%Y-%m-%d")))
|
|
||||||
endif
|
|
||||||
|
|
||||||
PUBLISHABLES += publish-aws-s3
|
|
||||||
TARGETS += publish-aws-s3
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifeq ($(RELEASE_TYPE),production)
|
|
||||||
ifdef PUBLISH_BINTRAY_DEBIAN
|
|
||||||
publish-bintray-debian: $(PUBLISH_BINTRAY_DEBIAN)
|
|
||||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/bintray.sh \
|
|
||||||
-f $(publishable) \
|
|
||||||
-v $(APPLICATION_VERSION_DEBIAN) \
|
|
||||||
-r $(TARGET_ARCH) \
|
|
||||||
-t $(RELEASE_TYPE) \
|
|
||||||
-o $(BINTRAY_ORGANIZATION) \
|
|
||||||
-p $(BINTRAY_REPOSITORY_DEBIAN) \
|
|
||||||
-c $(BINTRAY_COMPONENT) \
|
|
||||||
-y debian))
|
|
||||||
|
|
||||||
PUBLISHABLES += publish-bintray-debian
|
|
||||||
TARGETS += publish-bintray-debian
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifdef PUBLISH_BINTRAY_REDHAT
|
|
||||||
publish-bintray-redhat: $(PUBLISH_BINTRAY_REDHAT)
|
|
||||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/bintray.sh \
|
|
||||||
-f $(publishable) \
|
|
||||||
-v $(APPLICATION_VERSION_REDHAT) \
|
|
||||||
-r $(TARGET_ARCH) \
|
|
||||||
-t $(RELEASE_TYPE) \
|
|
||||||
-o $(BINTRAY_ORGANIZATION) \
|
|
||||||
-p $(BINTRAY_REPOSITORY_REDHAT) \
|
|
||||||
-c $(BINTRAY_COMPONENT) \
|
|
||||||
-y redhat))
|
|
||||||
|
|
||||||
PUBLISHABLES += publish-bintray-redhat
|
|
||||||
TARGETS += publish-bintray-redhat
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
publish-all: $(PUBLISHABLES)
|
|
||||||
|
|
||||||
.PHONY: $(TARGETS)
|
.PHONY: $(TARGETS)
|
||||||
|
|
||||||
cli-develop:
|
lint:
|
||||||
./scripts/build/dependencies-npm.sh \
|
npm run lint
|
||||||
-r "$(TARGET_ARCH)" \
|
|
||||||
-v "$(NODE_VERSION)" \
|
|
||||||
-t node \
|
|
||||||
-s "$(PLATFORM)"
|
|
||||||
|
|
||||||
electron-develop:
|
test:
|
||||||
./scripts/build/dependencies-npm.sh \
|
npm run test
|
||||||
-r "$(TARGET_ARCH)" \
|
|
||||||
-v "$(ELECTRON_VERSION)" \
|
|
||||||
-t electron \
|
|
||||||
-s "$(PLATFORM)"
|
|
||||||
|
|
||||||
sass:
|
|
||||||
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
|
|
||||||
|
|
||||||
lint-js:
|
|
||||||
eslint lib tests scripts bin versionist.conf.js webpack.config.js
|
|
||||||
|
|
||||||
lint-sass:
|
|
||||||
sass-lint lib/gui/scss
|
|
||||||
|
|
||||||
lint-cpp:
|
|
||||||
cpplint --recursive src
|
|
||||||
|
|
||||||
lint-html:
|
|
||||||
node scripts/html-lint.js
|
|
||||||
|
|
||||||
lint-spell:
|
|
||||||
codespell \
|
|
||||||
--dictionary - \
|
|
||||||
--dictionary dictionary.txt \
|
|
||||||
--skip *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
|
|
||||||
lib tests docs scripts Makefile *.md LICENSE
|
|
||||||
|
|
||||||
lint: lint-js lint-sass lint-cpp lint-html lint-spell
|
|
||||||
|
|
||||||
MOCHA_OPTIONS=--recursive --reporter spec
|
|
||||||
|
|
||||||
test-spectron:
|
|
||||||
ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron
|
|
||||||
|
|
||||||
test-gui:
|
|
||||||
electron-mocha $(MOCHA_OPTIONS) --renderer tests/gui
|
|
||||||
|
|
||||||
test-sdk:
|
|
||||||
electron-mocha $(MOCHA_OPTIONS) \
|
|
||||||
tests/shared \
|
|
||||||
tests/image-stream
|
|
||||||
|
|
||||||
test-cli:
|
|
||||||
mocha $(MOCHA_OPTIONS) \
|
|
||||||
tests/shared \
|
|
||||||
tests/image-stream
|
|
||||||
|
|
||||||
test: test-gui test-sdk test-spectron
|
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Available targets: $(TARGETS)"
|
@echo "Available targets: $(TARGETS)"
|
||||||
|
|
||||||
info:
|
info:
|
||||||
@echo "Application version : $(APPLICATION_VERSION)"
|
|
||||||
@echo "Release type : $(RELEASE_TYPE)"
|
|
||||||
@echo "Platform : $(PLATFORM)"
|
@echo "Platform : $(PLATFORM)"
|
||||||
@echo "Host arch : $(HOST_ARCH)"
|
@echo "Host arch : $(HOST_ARCH)"
|
||||||
@echo "Target arch : $(TARGET_ARCH)"
|
@echo "Target arch : $(TARGET_ARCH)"
|
||||||
|
|
||||||
sanity-checks:
|
|
||||||
./scripts/ci/ensure-staged-sass.sh
|
|
||||||
./scripts/ci/ensure-staged-shrinkwrap.sh
|
|
||||||
./scripts/ci/ensure-npm-dependencies-compatibility.sh
|
|
||||||
./scripts/ci/ensure-npm-valid-dependencies.sh
|
|
||||||
./scripts/ci/ensure-npm-shrinkwrap-versions.sh
|
|
||||||
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIRECTORY)
|
rm -rf $(BUILD_DIRECTORY)
|
||||||
|
|
||||||
distclean: clean
|
distclean: clean
|
||||||
rm -rf node_modules
|
rm -rf node_modules
|
||||||
rm -rf build
|
rm -rf dist
|
||||||
rm -rf generated
|
rm -rf generated
|
||||||
|
rm -rf $(BUILD_TEMPORARY_DIRECTORY)
|
||||||
|
|
||||||
.DEFAULT_GOAL = help
|
.DEFAULT_GOAL = help
|
||||||
|
|||||||
236
README.md
@@ -1,123 +1,192 @@
|
|||||||
Etcher
|
# Etcher
|
||||||
======
|
|
||||||
|
|
||||||
> Flash OS images to SD cards & USB drives, safely and easily.
|
> Flash OS images to SD cards & USB drives, safely and easily.
|
||||||
|
|
||||||
Etcher is a powerful OS image flasher built with web technologies to ensure
|
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
|
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
|
you from accidentally writing to your hard-drives, ensures every byte of data
|
||||||
was written correctly and much more.
|
was written correctly, and much more. It can also 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://etcher.io)
|
[](https://balena.io/etcher)
|
||||||

|
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||||
[](https://travis-ci.org/resin-io/etcher/branches)
|
[](https://forums.balena.io/c/etcher)
|
||||||
[](https://ci.appveyor.com/project/resin-io/etcher/branch/master)
|
|
||||||
[](https://david-dm.org/resin-io/etcher)
|
|
||||||
[](https://forums.resin.io/c/etcher)
|
|
||||||
[](https://waffle.io/resin-io/etcher)
|
|
||||||
|
|
||||||
***
|
---
|
||||||
|
|
||||||
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones] | [**CLI**][CLI]
|
[**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
|
||||||
|
|
||||||

|
## Supported Operating Systems
|
||||||
|
|
||||||
Supported Operating Systems
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
- Linux (most distros)
|
- Linux (most distros)
|
||||||
- macOS 10.9 and later
|
- macOS 10.10 (Yosemite) and later
|
||||||
- Microsoft Windows 7 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
|
[Electron][electron]. Read more in their
|
||||||
[documentation][electron-supported-platforms].
|
[documentation][electron-supported-platforms].
|
||||||
|
|
||||||
Installers
|
## Installers
|
||||||
----------
|
|
||||||
|
|
||||||
Refer to the [downloads page][etcher] for the latest pre-made
|
Refer to the [downloads page][etcher] for the latest pre-made
|
||||||
installers for all supported operating systems.
|
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)
|
#### 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)
|
||||||
|
|
||||||
```
|
1. Add Etcher Debian repository:
|
||||||
echo "deb https://dl.bintray.com/resin-io/debian stable etcher" | sudo tee /etc/apt/sources.list.d/etcher.list
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Trust Bintray.com's GPG key:
|
```sh
|
||||||
|
curl -1sLf \
|
||||||
|
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
|
||||||
|
| sudo -E bash
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
2. Update and install:
|
||||||
sudo apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 379CE192D401AB61
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Update and install:
|
```sh
|
||||||
|
sudo apt-get update
|
||||||
```sh
|
sudo apt-get install balena-etcher-electron
|
||||||
sudo apt-get update
|
```
|
||||||
sudo apt-get install etcher-electron
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Uninstall
|
##### Uninstall
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt-get remove etcher-electron
|
sudo apt-get remove balena-etcher-electron
|
||||||
sudo rm /etc/apt/sources.list.d/etcher.list
|
rm /etc/apt/sources.list.d/balena-etcher.list
|
||||||
sudo apt-get update
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
apt-get update
|
||||||
```
|
```
|
||||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
|
||||||
|
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
|
||||||
|
|
||||||
|
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
|
||||||
|
|
||||||
|
|
||||||
|
##### DNF
|
||||||
|
|
||||||
1. Add Etcher rpm repository:
|
1. Add Etcher rpm repository:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo wget https://bintray.com/resin-io/redhat/rpm -O /etc/yum.repos.d/bintray-resin-io-redhat.repo
|
curl -1sLf \
|
||||||
```
|
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||||
|
| sudo -E bash
|
||||||
|
```
|
||||||
|
|
||||||
2. Update and install:
|
2. Update and install:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum install -y etcher-electron
|
sudo dnf install -y balena-etcher-electron
|
||||||
```
|
```
|
||||||
or
|
|
||||||
```sh
|
###### Uninstall
|
||||||
sudo dnf install -y etcher-electron
|
|
||||||
```
|
```sh
|
||||||
|
rm /etc/yum.repos.d/balena-etcher.repo
|
||||||
|
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Yum
|
||||||
|
|
||||||
|
1. Add Etcher rpm repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -1sLf \
|
||||||
|
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||||
|
| sudo -E bash
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update and install:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo yum install -y balena-etcher-electron
|
||||||
|
```
|
||||||
|
|
||||||
|
###### Uninstall
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo yum remove -y balena-etcher-electron
|
||||||
|
rm /etc/yum.repos.d/balena-etcher.repo
|
||||||
|
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OpenSUSE LEAP & Tumbleweed install (zypper)
|
||||||
|
|
||||||
|
1. Add the repo
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -1sLf \
|
||||||
|
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||||
|
| sudo -E bash
|
||||||
|
```
|
||||||
|
2. Update and install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo zypper up
|
||||||
|
sudo zypper install balena-etcher-electron
|
||||||
|
```
|
||||||
|
|
||||||
##### Uninstall
|
##### Uninstall
|
||||||
|
|
||||||
```
|
```sh
|
||||||
sudo yum remove -y etcher-electron
|
sudo zypper rm balena-etcher-electron
|
||||||
sudo rm /etc/yum.repos.d/bintray-resin-io-redhat.repo
|
# remove the repo
|
||||||
sudo yum clean all
|
sudo zypper rr balena-etcher
|
||||||
sudo yum makecache fast
|
sudo zypper rr balena-etcher-source
|
||||||
```
|
|
||||||
or
|
|
||||||
```
|
|
||||||
sudo dnf remove -y etcher-electron
|
|
||||||
sudo rm /etc/yum.repos.d/bintray-resin-io-redhat.repo
|
|
||||||
sudo dnf clean all
|
|
||||||
sudo dnf makecache
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Brew Cask (macOS)
|
#### Solus (GNU/Linux x64)
|
||||||
|
|
||||||
Note that the Etcher Cask has to be updated manually to point to new versions,
|
```sh
|
||||||
|
sudo eopkg it etcher
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Uninstall
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo eopkg rm etcher
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Uninstall
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yay -R balena-etcher
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Brew (macOS)
|
||||||
|
|
||||||
|
**Note**: Etcher has to be updated manually to point to new versions,
|
||||||
so it might not refer to the latest version immediately after an Etcher
|
so it might not refer to the latest version immediately after an Etcher
|
||||||
release.
|
release.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
brew cask install etcher
|
brew install balenaetcher
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Uninstall
|
##### Uninstall
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
brew cask uninstall etcher
|
brew uninstall balenaetcher
|
||||||
```
|
```
|
||||||
|
|
||||||
### Chocolatey (Windows)
|
#### Chocolatey (Windows)
|
||||||
|
|
||||||
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
|
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
|
||||||
is kept up to date automatically.
|
is kept up to date automatically.
|
||||||
@@ -126,25 +195,28 @@ is kept up to date automatically.
|
|||||||
choco install etcher
|
choco install etcher
|
||||||
```
|
```
|
||||||
|
|
||||||
Support
|
##### Uninstall
|
||||||
-------
|
|
||||||
|
|
||||||
If you're having any problem, please [raise an issue][newissue] on GitHub and
|
```sh
|
||||||
the resin.io team will be happy to help.
|
choco uninstall etcher
|
||||||
|
```
|
||||||
|
|
||||||
License
|
## Support
|
||||||
-------
|
|
||||||
|
|
||||||
Etcher is free software, and may be redistributed under the terms specified in
|
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
|
||||||
the [license].
|
the [license].
|
||||||
|
|
||||||
[etcher]: https://etcher.io
|
[etcher]: https://balena.io/etcher
|
||||||
[electron]: http://electron.atom.io
|
[electron]: https://electronjs.org/
|
||||||
[electron-supported-platforms]: http://electron.atom.io/docs/tutorial/supported-platforms/
|
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||||
[SUPPORT]: https://github.com/resin-io/etcher/blob/master/SUPPORT.md
|
[support]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
||||||
[CONTRIBUTING]: https://github.com/resin-io/etcher/blob/master/docs/CONTRIBUTING.md
|
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||||
[CLI]: https://github.com/resin-io/etcher/blob/master/docs/CLI.md
|
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||||
[USER-DOCUMENTATION]: https://github.com/resin-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
[milestones]: https://github.com/balena-io/etcher/milestones
|
||||||
[milestones]: https://github.com/resin-io/etcher/milestones
|
[newissue]: https://github.com/balena-io/etcher/issues/new
|
||||||
[newissue]: https://github.com/resin-io/etcher/issues/new
|
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE
|
||||||
[license]: https://github.com/resin-io/etcher/blob/master/LICENSE
|
|
||||||
|
|||||||
27
SUPPORT.md
@@ -1,34 +1,43 @@
|
|||||||
Getting help with Etcher
|
Getting help with BalenaEtcher
|
||||||
========================
|
===============================
|
||||||
|
|
||||||
There are various ways to get support for Etcher if you experience an issue or
|
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.
|
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
|
Forums
|
||||||
------
|
------
|
||||||
|
|
||||||
We have a [Discourse forum][discourse] which is open to everyone, so please
|
We have a [Discourse forum][discourse] which is open to everyone, so please
|
||||||
come join us :). Drop us a line there and the resin.io staff and community
|
come join us :). Drop us a line there and the balena.io staff and community
|
||||||
users will be happy to assist. Your question might already be answered, so take
|
users will be happy to assist. Your question might already be answered, so take
|
||||||
a look at the existing threads before opening a new one!
|
a look at the existing threads before opening a new one!
|
||||||
|
|
||||||
Make sure to mention the following information to help us provide better
|
Make sure to mention the following information to help us provide better
|
||||||
support:
|
support:
|
||||||
|
|
||||||
- The Etcher version you're running.
|
- The BalenaEtcher version you're running.
|
||||||
|
|
||||||
- The operating system you're running Etcher in.
|
- The operating system you're running Etcher in.
|
||||||
|
|
||||||
- Relevant logging output, if any, from DevTools, which you can open by
|
- Relevant logging output, if any, from DevTools, which you can open by
|
||||||
pressing `Ctrl+Alt+I` or `Cmd+Alt+I` depending on your platform.
|
pressing `Ctrl+Shift+I` or `Cmd+Alt+I` depending on your platform.
|
||||||
|
|
||||||
GitHub
|
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
|
tracker][issues] and if there isn't a ticket covering it, [create
|
||||||
one][new-issue].
|
one][new-issue].
|
||||||
|
|
||||||
[discourse]: https://forums.resin.io/c/etcher
|
[discourse]: https://forums.balena.io/c/etcher
|
||||||
[issues]: https://github.com/resin-io/etcher/issues
|
[issues]: https://github.com/balena-io/etcher/issues
|
||||||
[new-issue]: https://github.com/resin-io/etcher/issues/new
|
[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
|
||||||
31
afterPack.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const cp = require('child_process')
|
||||||
|
const fs = require('fs')
|
||||||
|
const outdent = require('outdent')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
exports.default = function(context) {
|
||||||
|
if (context.packager.platform.name !== 'linux') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const scriptPath = path.join(context.appOutDir, context.packager.executableName)
|
||||||
|
const binPath = scriptPath + '.bin'
|
||||||
|
cp.execFileSync('mv', [scriptPath, binPath])
|
||||||
|
fs.writeFileSync(
|
||||||
|
scriptPath,
|
||||||
|
outdent({trimTrailingNewline: false})`
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Resolve symlinks. Warning, readlink -f doesn't work on MacOS/BSD
|
||||||
|
script_dir="$(dirname "$(readlink -f "\${BASH_SOURCE[0]}")")"
|
||||||
|
|
||||||
|
if [[ $EUID -ne 0 ]] || [[ $ELECTRON_RUN_AS_NODE ]]; then
|
||||||
|
"\${script_dir}"/${context.packager.executableName}.bin "$@"
|
||||||
|
else
|
||||||
|
"\${script_dir}"/${context.packager.executableName}.bin "$@" --no-sandbox
|
||||||
|
fi
|
||||||
|
`
|
||||||
|
)
|
||||||
|
cp.execFileSync('chmod', ['+x', scriptPath])
|
||||||
|
}
|
||||||
23
afterSignHook.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { notarize } = require('electron-notarize')
|
||||||
|
const { ELECTRON_SKIP_NOTARIZATION } = process.env
|
||||||
|
|
||||||
|
async function main(context) {
|
||||||
|
const { electronPlatformName, appOutDir } = context
|
||||||
|
if (electronPlatformName !== 'darwin' || ELECTRON_SKIP_NOTARIZATION === 'true') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename
|
||||||
|
const appleId = 'accounts+apple@balena.io'
|
||||||
|
|
||||||
|
await notarize({
|
||||||
|
appBundleId: 'io.balena.etcher',
|
||||||
|
appPath: `${appOutDir}/${appName}.app`,
|
||||||
|
appleId,
|
||||||
|
appleIdPassword: `@keychain:Application Loader: ${appleId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.default = main
|
||||||
50
appveyor.yml
@@ -1,50 +0,0 @@
|
|||||||
# appveyor file
|
|
||||||
# http://www.appveyor.com/docs/appveyor-yml
|
|
||||||
|
|
||||||
image: Visual Studio 2015
|
|
||||||
|
|
||||||
# See https://github.com/electron/spectron#on-appveyor
|
|
||||||
os: unstable
|
|
||||||
|
|
||||||
cache:
|
|
||||||
- C:\Users\appveyor\.node-gyp
|
|
||||||
- '%LOCALAPPDATA%\electron\Cache'
|
|
||||||
- '%LOCALAPPDATA%\electron-builder\cache'
|
|
||||||
- '%AppData%\npm-cache'
|
|
||||||
- '%USERPROFILE%\.pkg-cache'
|
|
||||||
- node_modules -> npm-shrinkwrap.json
|
|
||||||
- C:\ProgramData\chocolatey\bin -> appveyor.yml
|
|
||||||
- C:\ProgramData\chocolatey\lib -> appveyor.yml
|
|
||||||
- C:\Users\appveyor\AppData\Local\Temp\chocolatey -> appveyor.yml
|
|
||||||
|
|
||||||
# what combinations to test
|
|
||||||
environment:
|
|
||||||
global:
|
|
||||||
ELECTRON_NO_ATTACH_CONSOLE: true
|
|
||||||
nodejs_version: "6.10.3"
|
|
||||||
|
|
||||||
platform:
|
|
||||||
- x86
|
|
||||||
- x64
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
|
||||||
|
|
||||||
install:
|
|
||||||
- ps: Update-NodeJsInstallation $env:nodejs_version $env:Platform
|
|
||||||
- set PATH=C:\Program Files (x86)\Windows Kits\8.1\bin\x86;%PATH%
|
|
||||||
- set PATH=C:\Program Files (x86)\NSIS;%PATH%
|
|
||||||
- set PATH=C:\MinGW\bin;%PATH%
|
|
||||||
- set PATH=C:\MinGW\msys\1.0\bin;%PATH%
|
|
||||||
- bash .\scripts\ci\install.sh -o win32 -r %Platform%
|
|
||||||
|
|
||||||
build: off
|
|
||||||
|
|
||||||
test_script:
|
|
||||||
- node --version
|
|
||||||
- npm --version
|
|
||||||
- bash .\scripts\ci\test.sh -o win32 -r %Platform%
|
|
||||||
- bash .\scripts\ci\build-installers.sh -o win32 -r %Platform%
|
|
||||||
|
|
||||||
deploy_script:
|
|
||||||
- if %APPVEYOR_REPO_BRANCH%==master (bash .\scripts\ci\deploy.sh -o win32 -r %Platform%)
|
|
||||||
BIN
assets/dmg/background.png
Normal file → Executable file
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 38 KiB |
BIN
assets/dmg/background.tiff
Normal file → Executable file
BIN
assets/dmg/background@2x.png
Normal file → Executable file
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 49 KiB |
BIN
assets/icon.icns
Normal file → Executable file
BIN
assets/icon.ico
Normal file → Executable file
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 361 KiB |
BIN
assets/icon.png
Normal file → Executable file
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 881 B After Width: | Height: | Size: 479 B |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 802 B |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
require('../lib/cli/etcher');
|
|
||||||
25
binding.gyp
@@ -1,25 +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",
|
|
||||||
],
|
|
||||||
} ]
|
|
||||||
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
4
dev-app-update.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
owner: balena-io
|
||||||
|
repo: etcher
|
||||||
|
provider: github
|
||||||
|
updaterCacheDirName: balena-etcher-updater
|
||||||
@@ -12,12 +12,9 @@ technologies used in Etcher that you should become familiar with:
|
|||||||
|
|
||||||
- [Electron][electron]
|
- [Electron][electron]
|
||||||
- [NodeJS][nodejs]
|
- [NodeJS][nodejs]
|
||||||
- [AngularJS][angularjs]
|
|
||||||
- [Redux][redux]
|
- [Redux][redux]
|
||||||
- [ImmutableJS][immutablejs]
|
- [ImmutableJS][immutablejs]
|
||||||
- [Bootstrap][bootstrap]
|
|
||||||
- [Sass][sass]
|
- [Sass][sass]
|
||||||
- [Flexbox Grid][flexbox-grid]
|
|
||||||
- [Mocha][mocha]
|
- [Mocha][mocha]
|
||||||
- [JSDoc][jsdoc]
|
- [JSDoc][jsdoc]
|
||||||
|
|
||||||
@@ -40,62 +37,18 @@ to submit their work or bug reports.
|
|||||||
|
|
||||||
These are the main Etcher components, in a nutshell:
|
These are the main Etcher components, in a nutshell:
|
||||||
|
|
||||||
- [Etcher Image Write][etcher-image-write]
|
- [Drivelist](https://github.com/balena-io-modules/drivelist)
|
||||||
|
|
||||||
This is the repository that implements the actual procedures to write an image
|
|
||||||
to a raw device and the place where image validation resides. Its main purpose
|
|
||||||
is to abstract the messy details of interacting with raw devices in all major
|
|
||||||
operating systems.
|
|
||||||
|
|
||||||
- [Etcher Image Stream](../lib/image-stream)
|
|
||||||
|
|
||||||
> (Moved from a separate repository into the main Etcher codebase)
|
|
||||||
|
|
||||||
This module converts any kind of input into a readable stream
|
|
||||||
representing the image so it can be plugged to [etcher-image-write]. Inputs
|
|
||||||
that this module might handle could be, for example: a simple image file, a URL
|
|
||||||
to an image, a compressed image, an image inside a ZIP archive, etc. Together
|
|
||||||
with [etcher-image-write], these modules are the building blocks needed to take
|
|
||||||
an image representation to the user's device, the "Etcher's backend".
|
|
||||||
|
|
||||||
- [Drivelist](https://github.com/resin-io-modules/drivelist)
|
|
||||||
|
|
||||||
As the name implies, this module's duty is to detect the connected drives
|
As the name implies, this module's duty is to detect the connected drives
|
||||||
uniformly in all major operating systems, along with valuable metadata, like if
|
uniformly in all major operating systems, along with valuable metadata, like if
|
||||||
a drive is removable or not, to prevent users from trying to write an image to
|
a drive is removable or not, to prevent users from trying to write an image to
|
||||||
a system drive.
|
a system drive.
|
||||||
|
|
||||||
- [Etcher](https://github.com/resin-io/etcher)
|
- [Etcher](https://github.com/balena-io/etcher)
|
||||||
|
|
||||||
This is the *"main repository"*, from which you're reading this from, which is
|
This is the *"main repository"*, from which you're reading this from, which is
|
||||||
basically the front-end and glue for all previously listed projects.
|
basically the front-end and glue for all previously listed projects.
|
||||||
|
|
||||||
Front-ends
|
|
||||||
----------
|
|
||||||
|
|
||||||
The main repository consists of the implementation of the Etcher CLI and the
|
|
||||||
Etcher GUI (the desktop application), located at [`lib/cli/`][cli-dir] and
|
|
||||||
[`lib/gui/`][gui-dir], respectively.
|
|
||||||
|
|
||||||
In fact, the only front-end that interacts directly with Etcher's backend is
|
|
||||||
the CLI. The GUI merely forks the CLI and communicates with its child process
|
|
||||||
to get state information.
|
|
||||||
|
|
||||||
In this sense, you can consider the GUI as being the front-end to the CLI,
|
|
||||||
which is in turn the front-end to the actual image writing functionality.
|
|
||||||
|
|
||||||
As a way to simplify how the GUI forks the CLI in a packaged and distributed
|
|
||||||
context, both the CLI and GUI share the same application entry point. This
|
|
||||||
means that the same Etcher binary can behave as CLI or GUI as needed.
|
|
||||||
|
|
||||||
## Process communication
|
|
||||||
|
|
||||||
As mentioned before, the Etcher GUI forks the CLI and retrieves information
|
|
||||||
from it to update its state. In order to accomplish this, the Etcher CLI
|
|
||||||
contains certain features to ease communication:
|
|
||||||
|
|
||||||
- [Well-documented exit codes.][exit-codes]
|
|
||||||
|
|
||||||
Summary
|
Summary
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@@ -106,17 +59,12 @@ since fresh eyes could help unveil things that we take for granted, but should
|
|||||||
be documented instead!
|
be documented instead!
|
||||||
|
|
||||||
[lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328
|
[lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328
|
||||||
[etcher-image-write]: https://github.com/resin-io-modules/etcher-image-write
|
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/shared/exit-codes.js
|
||||||
[exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/shared/exit-codes.js
|
[gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui
|
||||||
[cli-dir]: https://github.com/resin-io/etcher/tree/master/lib/cli
|
|
||||||
[gui-dir]: https://github.com/resin-io/etcher/tree/master/lib/gui
|
|
||||||
[electron]: http://electron.atom.io
|
[electron]: http://electron.atom.io
|
||||||
[nodejs]: https://nodejs.org
|
[nodejs]: https://nodejs.org
|
||||||
[angularjs]: https://angularjs.org
|
|
||||||
[redux]: http://redux.js.org
|
[redux]: http://redux.js.org
|
||||||
[immutablejs]: http://facebook.github.io/immutable-js/
|
[immutablejs]: http://facebook.github.io/immutable-js/
|
||||||
[bootstrap]: http://getbootstrap.com
|
|
||||||
[sass]: http://sass-lang.com
|
[sass]: http://sass-lang.com
|
||||||
[flexbox-grid]: http://flexboxgrid.com
|
|
||||||
[mocha]: http://mochajs.org
|
[mocha]: http://mochajs.org
|
||||||
[jsdoc]: http://usejsdoc.org
|
[jsdoc]: http://usejsdoc.org
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
### macOS and GNU/Linux
|
|
||||||
|
|
||||||
- Extract the `.tar.gz` package by running:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
tar fvx path/to/cli.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
- Move the resulting directory to `/opt/etcher-cli`
|
|
||||||
|
|
||||||
- Add `/opt/etcher-cli` to the `PATH`. For example, add the following to
|
|
||||||
`.bashrc` or `.zshrc`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
export PATH="$PATH:/opt/etcher-cli"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
- Unzip the `.zip` package by right-clicking on it and selecting "Extract All"
|
|
||||||
|
|
||||||
- Move the resulting directory to `C:\etcher-cli`
|
|
||||||
|
|
||||||
- Add `C:\etcher-cli` to the `%PATH%`
|
|
||||||
|
|
||||||
- On Windows 10 and Windows 8
|
|
||||||
- Open *Control Panel*
|
|
||||||
- Open *System*
|
|
||||||
- Click the *Advanced system settings* link
|
|
||||||
- Click *Environment Variables*
|
|
||||||
- Find the `PATH` environment variable, and click *Edit*
|
|
||||||
- Append `;C:\etcher-cli` to the environment variable value
|
|
||||||
- Click *OK*
|
|
||||||
|
|
||||||
- On Windows 7
|
|
||||||
- Right-click the *My Computer* icon
|
|
||||||
- Open the *Properties* menu
|
|
||||||
- Open the *Advanced* tab
|
|
||||||
- Click *Environment Variables*
|
|
||||||
- Find the `PATH` environment variable, and click *Edit*
|
|
||||||
- Append `;C:\etcher-cli` to the environment variable value
|
|
||||||
- Click *OK*
|
|
||||||
|
|
||||||
- Re-open `cmd.exe`, or PowerShell
|
|
||||||
|
|
||||||
### Running
|
|
||||||
|
|
||||||
```sh
|
|
||||||
etcher -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Options
|
|
||||||
|
|
||||||
```
|
|
||||||
--help, -h show help
|
|
||||||
--version, -v show version number
|
|
||||||
--drive, -d drive
|
|
||||||
--check, -c validate write
|
|
||||||
--yes, -y confirm non-interactively
|
|
||||||
--unmount, -u unmount on success
|
|
||||||
```
|
|
||||||
42
docs/CLI.md
@@ -1,42 +0,0 @@
|
|||||||
Etcher CLI
|
|
||||||
==========
|
|
||||||
|
|
||||||
The Etcher CLI is a command-line tool that aims to provide all the benefits of
|
|
||||||
the Etcher desktop application in a way that can be run from a terminal, or
|
|
||||||
even used from a script.
|
|
||||||
|
|
||||||
In fact, the Etcher desktop application is simply a wrapper around the CLI,
|
|
||||||
which is the place where the actual writing logic takes place.
|
|
||||||
|
|
||||||
Installing
|
|
||||||
----------
|
|
||||||
|
|
||||||
Head over to [etcher.io/cli][etcher-cli], download the package that corresponds to
|
|
||||||
your operating system, and then follow the installation instructions there.
|
|
||||||
|
|
||||||
Running
|
|
||||||
-------
|
|
||||||
|
|
||||||
```sh
|
|
||||||
etcher -v
|
|
||||||
```
|
|
||||||
|
|
||||||
Options
|
|
||||||
-------
|
|
||||||
|
|
||||||
```
|
|
||||||
--help, -h show help
|
|
||||||
--version, -v show version number
|
|
||||||
--drive, -d drive
|
|
||||||
--check, -c validate write
|
|
||||||
--yes, -y confirm non-interactively
|
|
||||||
--unmount, -u unmount on success
|
|
||||||
```
|
|
||||||
|
|
||||||
Debug mode
|
|
||||||
----------
|
|
||||||
|
|
||||||
You can set the `ETCHER_CLI_DEBUG` environment variable to make the Etcher CLI
|
|
||||||
print error stack traces.
|
|
||||||
|
|
||||||
[etcher-cli]: https://etcher.io/cli
|
|
||||||
@@ -56,11 +56,6 @@ The scope is required for types that make sense, such as `feat`, `fix`,
|
|||||||
`test`, etc. Certain commit types, such as `chore` might not have a clearly
|
`test`, etc. Certain commit types, such as `chore` might not have a clearly
|
||||||
defined scope, in which case its better to omit it.
|
defined scope, in which case its better to omit it.
|
||||||
|
|
||||||
When it applies, the scope must be either `GUI` or `CLI`.
|
|
||||||
|
|
||||||
A commit that takes part in both the GUI and CLI scopes, and makes more logical
|
|
||||||
sense that way, might entirely omit the scope.
|
|
||||||
|
|
||||||
Subject
|
Subject
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@@ -122,8 +117,8 @@ A commit can include multiple instances of this tag.
|
|||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
```
|
```
|
||||||
Closes: https://github.com/resin-io/etcher/issues/XXX
|
Closes: https://github.com/balena-io/etcher/issues/XXX
|
||||||
Fixes: https://github.com/resin-io/etcher/issues/XXX
|
Fixes: https://github.com/balena-io/etcher/issues/XXX
|
||||||
```
|
```
|
||||||
|
|
||||||
### `Change-Type: <type>`
|
### `Change-Type: <type>`
|
||||||
@@ -198,7 +193,7 @@ first non compressed extension.
|
|||||||
|
|
||||||
Change-Type: patch
|
Change-Type: patch
|
||||||
Changelog-Entry: Don't interpret image file name information between dots as image extensions.
|
Changelog-Entry: Don't interpret image file name information between dots as image extensions.
|
||||||
Fixes: https://github.com/resin-io/etcher/issues/492
|
Fixes: https://github.com/balena-io/etcher/issues/492
|
||||||
```
|
```
|
||||||
|
|
||||||
***
|
***
|
||||||
@@ -212,8 +207,8 @@ the operating system still thinks the drive has a file system.
|
|||||||
|
|
||||||
Change-Type: patch
|
Change-Type: patch
|
||||||
Changelog-Entry: Upgrade `etcher-image-write` to v5.0.2.
|
Changelog-Entry: Upgrade `etcher-image-write` to v5.0.2.
|
||||||
Link: https://github.com/resin-io-modules/etcher-image-write/blob/master/CHANGELOG.md#502---2016-06-27
|
Link: https://github.com/balena-io-modules/etcher-image-write/blob/master/CHANGELOG.md#502---2016-06-27
|
||||||
Fixes: https://github.com/resin-io/etcher/issues/531
|
Fixes: https://github.com/balena-io/etcher/issues/531
|
||||||
```
|
```
|
||||||
|
|
||||||
***
|
***
|
||||||
@@ -243,7 +238,7 @@ re-used by other services.
|
|||||||
|
|
||||||
Change-Type: minor
|
Change-Type: minor
|
||||||
Changelog-Entry: Check for updates and show a modal prompting the user to download the latest version.
|
Changelog-Entry: Check for updates and show a modal prompting the user to download the latest version.
|
||||||
Closes: https://github.com/resin-io/etcher/issues/396
|
Closes: https://github.com/balena-io/etcher/issues/396
|
||||||
```
|
```
|
||||||
|
|
||||||
[angular-commit-guidelines]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit
|
[angular-commit-guidelines]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ Developing
|
|||||||
|
|
||||||
#### Common
|
#### Common
|
||||||
|
|
||||||
- [NodeJS](https://nodejs.org) (at least v6)
|
- [NodeJS](https://nodejs.org) (at least v6.11)
|
||||||
- [Python 2.7](https://www.python.org)
|
- [Python 2.7](https://www.python.org)
|
||||||
- [jq](https://stedolan.github.io/jq/)
|
- [jq](https://stedolan.github.io/jq/)
|
||||||
- [curl](https://curl.haxx.se/)
|
- [curl](https://curl.haxx.se/)
|
||||||
|
- [npm](https://www.npmjs.com/) (version 6.7)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
@@ -51,10 +52,12 @@ The following MinGW packages are required:
|
|||||||
- `msys-bash`
|
- `msys-bash`
|
||||||
- `msys-coreutils`
|
- `msys-coreutils`
|
||||||
|
|
||||||
#### OS X
|
#### macOS
|
||||||
|
|
||||||
- [XCode](https://developer.apple.com/xcode/) or [XCode Command Line Tools],
|
- [Xcode](https://developer.apple.com/xcode/)
|
||||||
which can be installed by running `xcode-select --install`.
|
|
||||||
|
It's not enough to have [Xcode Command Line Tools] installed. Xcode must be installed
|
||||||
|
as well.
|
||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ which can be installed by running `xcode-select --install`.
|
|||||||
### Cloning the project
|
### Cloning the project
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/resin-io/etcher
|
git clone --recursive https://github.com/balena-io/etcher
|
||||||
cd etcher
|
cd etcher
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -88,17 +91,11 @@ make electron-develop
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Build the GUI
|
# Build the GUI
|
||||||
make webpack
|
npm run webpack
|
||||||
# Start Electron
|
# Start Electron
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
#### CLI
|
|
||||||
|
|
||||||
```sh
|
|
||||||
node bin/etcher
|
|
||||||
```
|
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@@ -204,7 +201,7 @@ Before your pull request can be merged, the following conditions must hold:
|
|||||||
|
|
||||||
- The linter doesn't throw any warning.
|
- The linter doesn't throw any warning.
|
||||||
|
|
||||||
- All the tests passes.
|
- All the tests pass.
|
||||||
|
|
||||||
- The coding style aligns with the project's convention.
|
- The coding style aligns with the project's convention.
|
||||||
|
|
||||||
@@ -213,9 +210,9 @@ systems we support.
|
|||||||
|
|
||||||
Don't hesitate to get in touch if you have any questions or need any help!
|
Don't hesitate to get in touch if you have any questions or need any help!
|
||||||
|
|
||||||
[ARCHITECTURE]: https://github.com/resin-io/etcher/blob/master/docs/ARCHITECTURE.md
|
[ARCHITECTURE]: https://github.com/balena-io/etcher/blob/master/docs/ARCHITECTURE.md
|
||||||
[COMMIT-GUIDELINES]: https://github.com/resin-io/etcher/blob/master/docs/COMMIT-GUIDELINES.md
|
[COMMIT-GUIDELINES]: https://github.com/balena-io/etcher/blob/master/docs/COMMIT-GUIDELINES.md
|
||||||
[EditorConfig]: http://editorconfig.org
|
[EditorConfig]: http://editorconfig.org
|
||||||
[shrinkwrap]: https://docs.npmjs.com/cli/shrinkwrap
|
[shrinkwrap]: https://docs.npmjs.com/cli/shrinkwrap
|
||||||
[hxd]: https://github.com/jhermsmeier/hxd
|
[hxd]: https://github.com/jhermsmeier/hxd
|
||||||
[XCode Command Line Tools]: https://developer.apple.com/library/content/technotes/tn2339/_index.html
|
[Xcode Command Line Tools]: https://developer.apple.com/library/content/technotes/tn2339/_index.html
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Releasing
|
|||||||
|
|
||||||
- [Prepare the new version](#preparing-a-new-version)
|
- [Prepare the new version](#preparing-a-new-version)
|
||||||
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
|
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
|
||||||
- [Draft a release on GitHub](https://github.com/resin-io/etcher/releases)
|
- [Draft a release on GitHub](https://github.com/balena-io/etcher/releases)
|
||||||
- Upload build artifacts to GitHub release draft
|
- Upload build artifacts to GitHub release draft
|
||||||
|
|
||||||
#### Testing
|
#### Testing
|
||||||
@@ -27,9 +27,10 @@ Releasing
|
|||||||
|
|
||||||
#### Publishing
|
#### Publishing
|
||||||
|
|
||||||
- [Publish release draft on GitHub](https://github.com/resin-io/etcher/releases)
|
- [Publish release draft on GitHub](https://github.com/balena-io/etcher/releases)
|
||||||
- [Post release note to forums](https://forums.resin.io/c/etcher)
|
- [Post release note to forums](https://forums.balena.io/c/etcher)
|
||||||
- [Update the website](https://github.com/resin-io/etcher-homepage)
|
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
|
||||||
|
- [Update the website](https://github.com/balena-io/etcher-homepage)
|
||||||
- Wait 2-3 hours for analytics (Sentry, Mixpanel) to trickle in and check for elevated error rates, or regressions
|
- Wait 2-3 hours for analytics (Sentry, Mixpanel) to trickle in and check for elevated error rates, or regressions
|
||||||
- If regressions arise; pull the release, and release a patched version, else:
|
- If regressions arise; pull the release, and release a patched version, else:
|
||||||
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
|
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
|
||||||
@@ -39,39 +40,6 @@ Releasing
|
|||||||
- Write a blog post about it, and / or
|
- Write a blog post about it, and / or
|
||||||
- Write about it to the Etcher mailing list
|
- Write about it to the Etcher mailing list
|
||||||
|
|
||||||
### Preparing a New Version
|
|
||||||
|
|
||||||
- Create & hop onto a new release branch, i.e. `release-1.0.0`
|
|
||||||
- Bump the version number in the `package.json`'s `version` property.
|
|
||||||
- Bump the version number in the `npm-shrinkwrap.json`'s `version` property
|
|
||||||
- Add a new entry to `CHANGELOG.md` by running `make changelog`
|
|
||||||
- Manually revise the `CHANGELOG.md` versionist output
|
|
||||||
- Update `screenshot.png` so it displays the latest version in the bottom
|
|
||||||
right corner
|
|
||||||
- Revise the `updates.semverRange` version in `package.json`
|
|
||||||
- Commit the changes with the version number as the commit title, including the `v` prefix, to `master`. For example:
|
|
||||||
|
|
||||||
**NOTE:** The version **MUST** be prefixed with a "v"
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git commit -m "v1.0.0" # not 1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
- Create an annotated tag for the new version. The commit title should equal the annotated tag name. For example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git tag -a v1.0.0 -m "v1.0.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Push the commit and the annotated tag.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git push
|
|
||||||
git push --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
- Open a pull request against `master` titled "Release v1.0.0"
|
|
||||||
|
|
||||||
### Generating binaries
|
### Generating binaries
|
||||||
|
|
||||||
**Environment**
|
**Environment**
|
||||||
@@ -104,8 +72,6 @@ export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
|||||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||||
# Build AppImages
|
# Build AppImages
|
||||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||||
# Build CLI
|
|
||||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production cli-installer-tar-gz"
|
|
||||||
|
|
||||||
# x86
|
# x86
|
||||||
|
|
||||||
@@ -115,8 +81,6 @@ export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
|||||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||||
# Build AppImages
|
# Build AppImages
|
||||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||||
# Build CLI
|
|
||||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production cli-installer-tar-gz"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Mac OS
|
#### Mac OS
|
||||||
@@ -124,13 +88,9 @@ export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
|||||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||||
and set `CSC_NAME` to generate signed binaries on Mac OS.
|
and set `CSC_NAME` to generate signed binaries on Mac OS.
|
||||||
|
|
||||||
**NOTE:** The CLI is not code-signed for either at this time.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make electron-develop
|
make electron-develop
|
||||||
|
|
||||||
# Build the CLI
|
|
||||||
make RELEASE_TYPE=production cli-installer-tar-gz
|
|
||||||
# Build the zip
|
# Build the zip
|
||||||
make RELEASE_TYPE=production electron-installer-app-zip
|
make RELEASE_TYPE=production electron-installer-app-zip
|
||||||
# Build the dmg
|
# Build the dmg
|
||||||
@@ -143,14 +103,11 @@ make RELEASE_TYPE=production electron-installer-dmg
|
|||||||
and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows.
|
and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows.
|
||||||
|
|
||||||
**NOTE:**
|
**NOTE:**
|
||||||
- The CLI is not code-signed for either at this time.
|
|
||||||
- Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`.
|
- Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make electron-develop
|
make electron-develop
|
||||||
|
|
||||||
# Build the CLI
|
|
||||||
make RELEASE_TYPE=production cli-installer-zip
|
|
||||||
# Build the Portable version
|
# Build the Portable version
|
||||||
make RELEASE_TYPE=production electron-installer-portable
|
make RELEASE_TYPE=production electron-installer-portable
|
||||||
# Build the Installer
|
# Build the Installer
|
||||||
@@ -165,10 +122,10 @@ export BINTRAY_API_KEY="youruserapikey"
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "debian" -y "debian" -r "x64" -f "dist/etcher-electron_1.2.1_amd64.deb"
|
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "debian" -y "debian" -r "x64" -f "dist/etcher-electron_1.2.1_amd64.deb"
|
||||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "debian" -y "debian" -r "x86" -f "dist/etcher-electron_1.2.1_i386.deb"
|
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "debian" -y "debian" -r "x86" -f "dist/etcher-electron_1.2.1_i386.deb"
|
||||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "redhat" -y "redhat" -r "x64" -f "dist/etcher-electron-1.2.1.x86_64.rpm"
|
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "redhat" -y "redhat" -r "x64" -f "dist/etcher-electron-1.2.1.x86_64.rpm"
|
||||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "redhat" -y "redhat" -r "x86" -f "dist/etcher-electron-1.2.1.i686.rpm"
|
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "redhat" -y "redhat" -r "x86" -f "dist/etcher-electron-1.2.1.i686.rpm"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Uploading binaries to Amazon S3
|
### Uploading binaries to Amazon S3
|
||||||
@@ -178,7 +135,7 @@ export S3_KEY="..."
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/publish/aws-s3.sh -b "resin-production-downloads" -v "1.2.1" -p "etcher" -f "dist/<filename>"
|
./scripts/publish/aws-s3.sh -b "balena-production-downloads" -v "1.2.1" -p "etcher" -f "dist/<filename>"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dealing with a Problematic Release
|
### Dealing with a Problematic Release
|
||||||
@@ -189,7 +146,7 @@ revert the problematic release as soon as possible, until the bugs are fixed.
|
|||||||
|
|
||||||
You can revert a version by deleting its builds from the S3 bucket and Bintray.
|
You can revert a version by deleting its builds from the S3 bucket and Bintray.
|
||||||
Refer to the `Makefile` for the up to date information about the S3 bucket
|
Refer to the `Makefile` for the up to date information about the S3 bucket
|
||||||
where we push builds to, and get in touch with the resin.io operations team to
|
where we push builds to, and get in touch with the balena.io operations team to
|
||||||
get write access to it.
|
get write access to it.
|
||||||
|
|
||||||
The Etcher update notifier dialog and the website only show the a certain
|
The Etcher update notifier dialog and the website only show the a certain
|
||||||
@@ -203,3 +160,15 @@ aws s3api delete-object --bucket <bucket name> --key <file name>
|
|||||||
```
|
```
|
||||||
|
|
||||||
The Bintray dashboard provides an easy way to delete a version's files.
|
The Bintray dashboard provides an easy way to delete a version's files.
|
||||||
|
|
||||||
|
|
||||||
|
### Submitting binaries to Symantec
|
||||||
|
|
||||||
|
- [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/)
|
||||||
|
- Fill out form:
|
||||||
|
- **Select Submission Type:** "Provide a direct download URL"
|
||||||
|
- **Name of the software being detected:** Etcher
|
||||||
|
- **Name of detection given by Symantec product:** WS.Reputation.1
|
||||||
|
- **Contact name:** Balena.io Ltd
|
||||||
|
- **E-mail address:** hello@etcher.io
|
||||||
|
- **Are you the creator or distributor of the software in question?** Yes
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Signing
|
|||||||
### OS X
|
### OS X
|
||||||
|
|
||||||
1. Get our Apple Developer ID certificate for signing applications distributed
|
1. Get our Apple Developer ID certificate for signing applications distributed
|
||||||
outside the Mac App Store from the resin.io Apple account.
|
outside the Mac App Store from the balena.io Apple account.
|
||||||
|
|
||||||
2. Install the Developer ID certificate to your Mac's Keychain by double
|
2. Install the Developer ID certificate to your Mac's Keychain by double
|
||||||
clicking on the certificate file.
|
clicking on the certificate file.
|
||||||
@@ -62,7 +62,7 @@ packaging for OS X.
|
|||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
1. Get access to our code signing certificate and decryption key as a resin.io
|
1. Get access to our code signing certificate and decryption key as a balena.io
|
||||||
employee by asking for it from the relevant people.
|
employee by asking for it from the relevant people.
|
||||||
|
|
||||||
2. Place the certificate in the root of the Etcher repository naming it
|
2. Place the certificate in the root of the Etcher repository naming it
|
||||||
@@ -118,7 +118,7 @@ Publishing to S3
|
|||||||
- [AWS CLI][aws-cli]
|
- [AWS CLI][aws-cli]
|
||||||
|
|
||||||
Make sure you have the [AWS CLI tool][aws-cli] installed and configured to
|
Make sure you have the [AWS CLI tool][aws-cli] installed and configured to
|
||||||
access resin.io's production or snapshot S3 bucket.
|
access balena.io's production or snapshot S3 bucket.
|
||||||
|
|
||||||
Run the following command to publish all files for the current combination of
|
Run the following command to publish all files for the current combination of
|
||||||
_platform_ and _arch_ (building them if necessary):
|
_platform_ and _arch_ (building them if necessary):
|
||||||
@@ -128,7 +128,7 @@ make publish-aws-s3
|
|||||||
```
|
```
|
||||||
|
|
||||||
Also add links to each AWS S3 file in [GitHub Releases][github-releases]. See
|
Also add links to each AWS S3 file in [GitHub Releases][github-releases]. See
|
||||||
[`v1.0.0-beta.17`](https://github.com/resin-io/etcher/releases/tag/v1.0.0-beta.17)
|
[`v1.0.0-beta.17`](https://github.com/balena-io/etcher/releases/tag/v1.0.0-beta.17)
|
||||||
as an example.
|
as an example.
|
||||||
|
|
||||||
Publishing to Homebrew Cask
|
Publishing to Homebrew Cask
|
||||||
@@ -143,12 +143,12 @@ Publishing to Homebrew Cask
|
|||||||
Announcing
|
Announcing
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Post messages to the [Etcher forum][resin-forum-etcher] announcing the new version
|
Post messages to the [Etcher forum][balena-forum-etcher] announcing the new version
|
||||||
of Etcher, and including the relevant section of the Changelog.
|
of Etcher, and including the relevant section of the Changelog.
|
||||||
|
|
||||||
[aws-cli]: https://aws.amazon.com/cli
|
[aws-cli]: https://aws.amazon.com/cli
|
||||||
[bintray]: https://bintray.com
|
[bintray]: https://bintray.com
|
||||||
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/etcher.rb
|
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/balenaetcher.rb
|
||||||
[homebrew-cask]: https://github.com/caskroom/homebrew-cask
|
[homebrew-cask]: https://github.com/caskroom/homebrew-cask
|
||||||
[resin-forum-etcher]: https://forums.resin.io/c/etcher
|
[balena-forum-etcher]: https://forums.balena.io/c/etcher
|
||||||
[github-releases]: https://github.com/resin-io/etcher/releases
|
[github-releases]: https://github.com/balena-io/etcher/releases
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ if you require this functionality, we advise to fallback to
|
|||||||
Deactivate desktop shortcut prompt on GNU/Linux
|
Deactivate desktop shortcut prompt on GNU/Linux
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
|
|
||||||
This is a feature provided by [AppImages](appimage), where the applications
|
This is a feature provided by [AppImages][appimage], where the applications
|
||||||
prompts the user to automatically register a desktop shortcut to easily access
|
prompts the user to automatically register a desktop shortcut to easily access
|
||||||
the application.
|
the application.
|
||||||
|
|
||||||
@@ -130,21 +130,6 @@ run Etcher on a GNU/Linux system.
|
|||||||
|
|
||||||
- liblzma (for xz decompression)
|
- liblzma (for xz decompression)
|
||||||
|
|
||||||
Simulate an update alert
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
You can set the `ETCHER_FAKE_S3_LATEST_VERSION` environment variable to a valid
|
|
||||||
semver version (greater than the current version) to trick the application into
|
|
||||||
thinking that what you put there is the latest available version, therefore
|
|
||||||
causing the update notification dialog to be presented at startup.
|
|
||||||
|
|
||||||
Note that the value of the variable will be ignored if it doesn't match the
|
|
||||||
release type of the current application version. For example, setting the
|
|
||||||
variable to a production version (e.g. `ETCHER_FAKE_S3_LATEST_VERSION=2.0.0`)
|
|
||||||
will be ignored if you're running a snapshot build, and vice-versa.
|
|
||||||
|
|
||||||
See [`PUBLISHING.md`][publishing] for more details about release types.
|
|
||||||
|
|
||||||
Recovering broken drives
|
Recovering broken drives
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
@@ -175,13 +160,25 @@ pre-installed in all modern Windows versions.
|
|||||||
- Run `clean`. This command will completely clean your drive by erasing any
|
- Run `clean`. This command will completely clean your drive by erasing any
|
||||||
existent filesystem.
|
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
|
### OS X
|
||||||
|
|
||||||
Run the following command in `Terminal.app`, replacing `N` by the corresponding
|
Run the following command in `Terminal.app`, replacing `N` by the corresponding
|
||||||
disk number, which you can find by running `diskutil list`:
|
disk number, which you can find by running `diskutil list`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
diskutil eraseDisk free UNTITLED /dev/diskN
|
diskutil eraseDisk FAT32 UNTITLED MBRFormat /dev/diskN
|
||||||
```
|
```
|
||||||
|
|
||||||
### GNU/Linux
|
### GNU/Linux
|
||||||
@@ -206,20 +203,16 @@ Running in older macOS versions
|
|||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
Etcher GUI is based on the [Electron][electron] framework, [which only supports
|
Etcher GUI is based on the [Electron][electron] framework, [which only supports
|
||||||
macOS 10.9 and newer versions][electron-supported-platforms].
|
macOS 10.10 (Yosemite) and newer versions][electron-supported-platforms].
|
||||||
|
|
||||||
You can however, run the [Etcher CLI][etcher-cli], which should work in older
|
[balena.io]: https://balena.io
|
||||||
platforms.
|
|
||||||
|
|
||||||
[resin.io]: https://resin.io
|
|
||||||
[appimage]: http://appimage.org
|
[appimage]: http://appimage.org
|
||||||
[xwayland]: https://wayland.freedesktop.org/xserver.html
|
[xwayland]: https://wayland.freedesktop.org/xserver.html
|
||||||
[weston.ini]: http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html
|
[weston.ini]: http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html
|
||||||
[diskpart]: https://technet.microsoft.com/en-us/library/cc770877(v=ws.11).aspx
|
[diskpart]: https://technet.microsoft.com/en-us/library/cc770877(v=ws.11).aspx
|
||||||
[electron]: http://electron.atom.io
|
[electron]: https://electronjs.org/
|
||||||
[electron-supported-platforms]: https://github.com/electron/electron/blob/master/docs/tutorial/supported-platforms.md
|
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||||
[etcher-cli]: https://github.com/resin-io/etcher/blob/master/docs/CLI.md
|
[publishing]: https://github.com/balena-io/etcher/blob/master/docs/PUBLISHING.md
|
||||||
[publishing]: https://github.com/resin-io/etcher/blob/master/docs/PUBLISHING.md
|
|
||||||
[windows-usb-tool]: https://www.microsoft.com/en-us/download/windows-usb-dvd-download-tool
|
[windows-usb-tool]: https://www.microsoft.com/en-us/download/windows-usb-dvd-download-tool
|
||||||
[rufus]: https://rufus.akeo.ie
|
[rufus]: https://rufus.akeo.ie
|
||||||
[unetbootin]: https://unetbootin.github.io
|
[unetbootin]: https://unetbootin.github.io
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
theme: jekyll-theme-minimal
|
|
||||||
178
docs/docs/getting-started.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
description: Getting started for etcherPro
|
||||||
|
slug: /
|
||||||
|
---
|
||||||
|
|
||||||
|
# EtcherPro User manual
|
||||||
|
|
||||||
|
## 1. Features
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
| Specifications | |
|
||||||
|
| --- | --- |
|
||||||
|
| Voltage in | 100 to 240 V AC - 10A - 50 to 60 Hz |
|
||||||
|
| Voltage out | 100 to 240 V AC - 10A - 50 to 60 Hz |
|
||||||
|
| Ports | 16 x USB 3.0, 16 X SD, 16 x mSD |
|
||||||
|
| Flashing capacity | 16 x targets when using an online image or, 15 x target drives and 1 x local source drive |
|
||||||
|
| Daisy-chaining | Up to 10 EtcherPro devices supported (160 x targets) |
|
||||||
|
| Language support | English |
|
||||||
|
| Software | balenaEtcher on balenaOS with auto-updates |
|
||||||
|
| Display | 7in RGB touch screen |
|
||||||
|
| Network connectivity | WiFi 2.4GHz, 5GHz |
|
||||||
|
| Working temperature | 5°C ~ 30°C |
|
||||||
|
| Certifications | CE, FCC |
|
||||||
|
|
||||||
|
## 2. Getting started
|
||||||
|
|
||||||
|
### 2.1 Setup
|
||||||
|
### Powering up the device
|
||||||
|
|
||||||
|
- EtcherPro is supplied with a mains power cable according to your region's plug standards
|
||||||
|
- On the back of the device, there are two groups of sockets, labelled as **IN** and **OUT**
|
||||||
|
- Plug the AC power cable to the socket labelled **POWER IN**, and then to the mains outlet
|
||||||
|
- The device should boot up automatically; wait until you see the Etcher interface show up
|
||||||
|
- [Warning] Please avoid using adaptors or extension leads, as this may damage the device or cause it to malfunction
|
||||||
|
|
||||||
|
### Connecting to WiFi
|
||||||
|
|
||||||
|
- The device will prompt you to connect to a local network the first time it boots (you can skip this step if you are not connecting to WiFi)
|
||||||
|
- Select the network to which you want to connect, type the password and select **OK**
|
||||||
|
- You can access the WiFi settings by selecting the WiFi icon at the top left corner of the screen
|
||||||
|
|
||||||
|
### 2.2 Etcher functions
|
||||||
|
|
||||||
|
### Flash from file
|
||||||
|
|
||||||
|
Using an image file as source to flash to one or multiple targets
|
||||||
|
|
||||||
|
- From the Etcher menu, select **Flash from file**
|
||||||
|
- Plug a drive that contains the image you would like to flash into the slot with the blue-colored blinking LED
|
||||||
|
- Select the image you want to flash from the file browser and select **OK**
|
||||||
|
- Plug at least one drive or device into an available slot. The plugged target(s) will be selected automatically and the LEDs will turn white
|
||||||
|
- Select **Flash**, to begin the flashing process
|
||||||
|
- The LED of each slot will first blink purple for flashing and then green for validating
|
||||||
|
- Once the flashing is complete, the LEDs will turn green for successfully flashed drives, and red for the failed ones
|
||||||
|
- You may safely unplug the drives when flashing is complete
|
||||||
|
|
||||||
|
### Flash from URL
|
||||||
|
|
||||||
|
Using an online image file as source to flash to one or multiple targets
|
||||||
|
|
||||||
|
- From the Etcher menu, select **Flash from URL**
|
||||||
|
- Enter the image URL of you would like to flash in the input field, and select **OK**
|
||||||
|
- Plug at least one drive or device into an available slot. The plugged target(s) will be selected automatically, and the LEDs will turn white
|
||||||
|
- Select **Flash**, to begin the flashing process
|
||||||
|
- The LED of each slot will first blink purple for flashing and then green for validating
|
||||||
|
- Once the flashing is complete, the LEDs will turn green for successfully flashed drives, and red for the failed ones
|
||||||
|
- You may safely unplug the drives when flashing is complete
|
||||||
|
|
||||||
|
### Clone drive
|
||||||
|
|
||||||
|
Using a drive as source, and cloning it to multiple drives
|
||||||
|
|
||||||
|
- From the Etcher menu, select **Clone drive**
|
||||||
|
- Plug a drive you would like to clone into the slot with the blue-colored blinking LED
|
||||||
|
- The drive will be selected automatically and the LED will stop blinking
|
||||||
|
- Plug at least one drive into an available slot. The plugged targets will be selected automatically, and the LEDs will turn white
|
||||||
|
- Select **Flash**, to begin the flashing process
|
||||||
|
- The LED of each slot will first blink purple for flashing and then green for validating
|
||||||
|
- Once the flashing is complete, the LEDs will turn green for successfully flashed drives, and red for the failed ones
|
||||||
|
- You may safely unplug the drives when flashing is complete
|
||||||
|
|
||||||
|
### Backup drive
|
||||||
|
|
||||||
|
Backing up one or multiple drives into another drive
|
||||||
|
|
||||||
|
- From the Etcher menu, select **more options**, then **Backup drive**
|
||||||
|
- Plug one or more drives you would like to backup into the slot(s) with the blue-colored blinking LED
|
||||||
|
- The drives will be autoselected and the LED will stop blinking
|
||||||
|
- Select **OK** to move on to the next step
|
||||||
|
- Plug the backup drive into the slot with the white-colored blinking LED. The plugged target will be automatically selected and the LED will stop blinking
|
||||||
|
- Select **Flash**, to begin the flashing process
|
||||||
|
- The LED of the target slot will first blink purple for flashing, and then green for validating
|
||||||
|
- Once the flashing is complete, the LED will turn green for a successful flash, and red for a failed one
|
||||||
|
- You may safely unplug the drive when flashing is complete
|
||||||
|
|
||||||
|
### Format drive
|
||||||
|
|
||||||
|
Formatting one or multiple drives
|
||||||
|
|
||||||
|
- From the Etcher menu, select **more options**, then **Format drive**
|
||||||
|
- Plug one or more drives you would like to format on the slots with the white-colored blinking LEDs
|
||||||
|
- The drives will be selected automatically, and the LED will stop blinking
|
||||||
|
- Select **Flash**, to begin the flashing process
|
||||||
|
- The LED of the target slot will first blink purple for flashing, and then green for validating
|
||||||
|
- Once the formatting is complete, the LEDs will turn green for successfully formatted drives, and red for the failed ones
|
||||||
|
- You may safely unplug the drive when formatting is complete
|
||||||
|
|
||||||
|
### 2.3 Daisy-chaining (power only)
|
||||||
|
|
||||||
|
Connecting up to 10 EtcherPro devices and power them from one socket.
|
||||||
|
|
||||||
|
- EtcherPro allows you to chain power (data chaining will be released later). To do this, you will need a male to female IEC C13/C14 power extension lead (min 10A rated) to connect one EtcherPro to another
|
||||||
|
- Plug the power extension lead to the 'POWER IN' side of the leading EtcherPro, and then to the 'POWER OUT' side of the successive EtcherPro
|
||||||
|
- The last EtcherPro of the stack should be plugged directly to the mains power socket
|
||||||
|
|
||||||
|
### 2.4 Sleep, wake and power off
|
||||||
|
|
||||||
|
- EtcherPro is set to automatically go into sleep mode after a few minutes
|
||||||
|
- You may change this setting by selecting the settings icon on the top right corner of the screen
|
||||||
|
- You may put the device to sleep manually by selecting the sleep button on the top left corner of the screen
|
||||||
|
- To wake up your device, just tap anywhere on the screen
|
||||||
|
- It is not necessary to power off your device, but if you would like to, you may simply unplug the power-in cable
|
||||||
|
|
||||||
|
### 3. Safety and handling
|
||||||
|
|
||||||
|
WARNING: Make sure you read and follow the safety and handling instructions before using EtcherPro in order to avoid the potential risk of causing damage to the device, electrical shock, fire, or damage to any other property. If EtcherPro gets physically damaged in any way, or you suspect liquid has leaked into the enclosure, unplug the power cable from the socket and avoid using the device before contacting support (pro@etcher.io).
|
||||||
|
Handling
|
||||||
|
It is important not to block the air vents on the back and bottom of the device. As such, we suggest setting up EtcherPro on a well supported desk, with plenty of surrounding space to ensure the device is properly ventilated while in use.
|
||||||
|
Liquid exposure
|
||||||
|
EtcherPro’s enclosure is not waterproof. It is important to keep liquids away from the device to avoid spillages. High humidity environments, rain or snow may also cause damage to the device.
|
||||||
|
Power
|
||||||
|
EtcherPro does not have an on/off switch. If you would like to power on the device, you need to plug the power cable into the mains socket. If you would like to power off the device, you need to unplug the power cable. Be sure to unplug the power cable from the socket if you suspect either the cable or the device is physically damaged in some way.
|
||||||
|
|
||||||
|
For your own protection and protection of the device, EtcherPro comes with a grounded AC power cable which only fits a grounded mains socket. If you don’t have a grounded socket installed, you should contact a specialist who can safely install an appropriate grounded socket. Do not attempt to power on the device without a connected grounding wire or with a power cable that does not meet the original specifications.
|
||||||
|
Repairing
|
||||||
|
EtcherPro is not meant to be serviced or repaired by the user. If your device has any issues you should contact support (pro@etcher.io). Attempting to disassemble the device will void the warranty, and could also cause injury or harm.
|
||||||
|
Radio interference
|
||||||
|
EtcherPro contains components and radios that emit electromagnetic fields. These electromagnetic fields may interfere with medical devices, such as pacemakers and defibrillators. Consult your physician and medical device manufacturer for information specific to your medical device and whether you need to maintain a safe distance of separation between your medical device(s) and EtcherPro. If you suspect EtcherPro is interfering with your medical device, stop using EtcherPro immediately.
|
||||||
|
|
||||||
|
EtcherPro emits electromagnetic fields due to the usage of components and radios. These fields can interfere with other devices and potentially cause them to malfunction. If you suspect EtcherPro is interfering with another device, unplug EtcherPro from the power and contact support (pro@etcher.io).
|
||||||
|
|
||||||
|
This equipment is not suitable for use in locations where children are likely to be present.
|
||||||
|
|
||||||
|
Atmospheric conditions (dust and vapor)
|
||||||
|
The EtcherPro enclosure is not sealed. Using the device in an environment that has increased amounts of dust, powder, vapors, corrosive substances, or other contaminants can cause malfunction, injury, and/or fire.
|
||||||
|
|
||||||
|
### 4. Warranty
|
||||||
|
|
||||||
|
**Limited Product Warranty**
|
||||||
|
Balena warrants that, for a period of one (1) year after the date of shipment, the Products will be free from defects in materials and workmanship under normal use. As Balena’s sole liability and Customer’s sole and exclusive remedy for any breach of the limited warranty set forth herein, Balena will, at its option and expense, repair or replace any Product returned to Customer during the warranty period that does not comply with such warranty, as confirmed by Balena. Replacement Products will be warranted for the remainder of the original warranty period or ninety (90) days, whichever is longer. All Products that are replaced become the property of Balena. Balena will have no obligation to the extent that any failure of a Product to comply with the limited warranty set forth in this limited product warranty results from or is otherwise attributable to: (i) negligence, misuse, or abuse of the Product; (ii) use of the Product other than in accordance with Balena’s published specifications or user manual; (iii) modifications, alterations or repairs to the Product made by a party other than Balena or a party authorized by Balena; (iv) any failure by Customer or a third party to comply with environmental and storage requirements for the Product specified by Balena, including, but not limited to, temperature or humidity ranges; or (v) use of the Product in combination with any third-party devices or products that have not been provided or recommended by Balena. The parties agree that Balena’s RMA Policy shall apply to Products returned pursuant to this limited product warranty for a breach of warranty.
|
||||||
|
|
||||||
|
THE LIMITED WARRANTY SET FORTH HEREIN IS IN LIEU OF, AND BALENA SPECIFICALLY DISCLAIMS, ANY AND ALL OTHER WARRANTIES AND CONDITIONS, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES OR CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR USAGE OF TRADE. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM BALENA OR ELSEWHERE, WILL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THESE TERMS.
|
||||||
|
|
||||||
|
The limited warranty does not apply to:
|
||||||
|
|
||||||
|
- Returned items that failed due to an accident, purchaser’s abuse, neglect or failure to operate in accordance with instructions provided in this refund policy.
|
||||||
|
- Returned items that failed due to incorrect voltage or improper wiring.
|
||||||
|
- Returned items that failed due to rain, excessive humidity, corrosive environments, or other contaminants.
|
||||||
|
- Any item damaged in shipment.
|
||||||
|
- Any product failure caused by installing or operating product under conditions not in accordance with installation and operation guidelines, or damaged by contact with tools or surroundings.
|
||||||
|
- Returned items with cosmetic defects that do not interfere with product functionality.
|
||||||
|
- Returned items that are incomplete or defaced.
|
||||||
|
- Returned items with a different serial number from what was authorized for return.
|
||||||
|
- Freight damaged items. If your shipment arrives damaged, you must note the damage on the carrier's delivery record in accordance with the carrier's policy, save the merchandise in the original box and packing it arrived in, and arrange for a carrier inspection of damaged merchandise.
|
||||||
|
|
||||||
|
**Initiating a warranty claim**
|
||||||
|
To initiate a warranty claim, please contact support (pro@etcher.io) to receive a copy of the RMA form. When filling the form, make sure you describe the issue as accurately as possible since it will be used as a basis for determining if the warranty claim is valid or not.
|
||||||
|
|
||||||
|
After Balena’s evaluation of the return item, Warranty or Out-of-Warranty status will be determined. If the description of the problem is the same as listed on Page 1 of the RMA form, the product will be repaired or replaced under warranty at no charge and shipped back, prepaid, to the customer.
|
||||||
|
|
||||||
|
If the description of the problem is different from the problem listed on Page 1 of the RMA form we will contact the customer. At such time, the customer must issue a written confirmation to proceed with the repair(s), agree to cover the costs of the repair and return freight, or authorize the product to be shipped back as is, at the customer’s expense. Failure to obtain written confirmation within thirty (30) days of notification will result in the product being returned as is, at the customer’s expense.
|
||||||
|
|
||||||
|
If the product has no identifiable problem, we reserve the right to charge for testing and return shipping costs.
|
||||||
|
|
||||||
|
For any product returned to balena for reasons other than warranty, a 20% restocking fee and round-trip shipping costs will be deducted from the credit refund. All returned items must be in their original box or crating and must include all packing material, manuals, and accessories.
|
||||||
BIN
docs/static/img/etcher-pro-rear-view.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
docs/static/img/etcher-pro-slots.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
docs/static/img/etcher-pro-top-view.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 411 KiB |
BIN
docs/static/img/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/logo.png
vendored
Executable file
|
After Width: | Height: | Size: 5.4 KiB |
@@ -1,20 +1,21 @@
|
|||||||
appId: io.resin.etcher
|
appId: io.balena.etcher
|
||||||
copyright: Copyright 2016-2018 Resinio Ltd
|
copyright: Copyright 2016-2021 Balena Ltd
|
||||||
productName: Etcher
|
productName: balenaEtcher
|
||||||
npmRebuild: false
|
npmRebuild: true
|
||||||
nodeGypRebuild: false
|
nodeGypRebuild: false
|
||||||
publish: null
|
publish: null
|
||||||
|
afterPack: "./afterPack.js"
|
||||||
|
asar: false
|
||||||
files:
|
files:
|
||||||
- lib
|
|
||||||
- "!lib/gui/app"
|
|
||||||
- lib/gui/app/index.html
|
|
||||||
- generated
|
- generated
|
||||||
- build/**/*.node
|
- lib/shared/catalina-sudo/sudo-askpass.osascript.js
|
||||||
- assets/icon.png
|
|
||||||
- node_modules/**/*
|
|
||||||
mac:
|
mac:
|
||||||
icon: assets/icon.icns
|
icon: assets/icon.icns
|
||||||
category: public.app-category.developer-tools
|
category: public.app-category.developer-tools
|
||||||
|
hardenedRuntime: true
|
||||||
|
entitlements: "entitlements.mac.plist"
|
||||||
|
entitlementsInherit: "entitlements.mac.plist"
|
||||||
|
artifactName: "${productName}-${version}.${ext}"
|
||||||
dmg:
|
dmg:
|
||||||
background: assets/dmg/background.tiff
|
background: assets/dmg/background.tiff
|
||||||
icon: assets/icon.icns
|
icon: assets/icon.icns
|
||||||
@@ -38,22 +39,21 @@ nsis:
|
|||||||
uninstallerIcon: assets/icon.ico
|
uninstallerIcon: assets/icon.ico
|
||||||
deleteAppDataOnUninstall: true
|
deleteAppDataOnUninstall: true
|
||||||
license: LICENSE
|
license: LICENSE
|
||||||
artifactName: "${productName}-Setup-${version}-${env.TARGET_ARCH}.${ext}"
|
artifactName: "${productName}-Setup-${version}.${ext}"
|
||||||
portable:
|
portable:
|
||||||
artifactName: "${productName}-Portable-${version}-${env.TARGET_ARCH}.${ext}"
|
artifactName: "${productName}-Portable-${version}.${ext}"
|
||||||
requestExecutionLevel: user
|
requestExecutionLevel: user
|
||||||
linux:
|
linux:
|
||||||
category: Utility
|
category: Utility
|
||||||
packageCategory: utils
|
packageCategory: utils
|
||||||
executableName: etcher-electron
|
executableName: balena-etcher-electron
|
||||||
synopsis: 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.
|
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
|
icon: assets/iconset
|
||||||
deb:
|
deb:
|
||||||
priority: optional
|
priority: optional
|
||||||
depends:
|
depends:
|
||||||
- gconf2
|
- gconf2
|
||||||
- gconf-service
|
- gconf-service
|
||||||
- libappindicator1
|
|
||||||
- libasound2
|
- libasound2
|
||||||
- libatk1.0-0
|
- libatk1.0-0
|
||||||
- libc6
|
- libc6
|
||||||
@@ -63,16 +63,17 @@ deb:
|
|||||||
- libexpat1
|
- libexpat1
|
||||||
- libfontconfig1
|
- libfontconfig1
|
||||||
- libfreetype6
|
- libfreetype6
|
||||||
|
- libgbm1
|
||||||
- libgcc1
|
- libgcc1
|
||||||
- libgconf-2-4
|
- libgconf-2-4
|
||||||
- libgdk-pixbuf2.0-0
|
- libgdk-pixbuf2.0-0
|
||||||
- libglib2.0-0
|
- libglib2.0-0
|
||||||
- libgtk2.0-0
|
- libgtk-3-0
|
||||||
- liblzma5
|
- liblzma5
|
||||||
- libnotify4
|
- libnotify4
|
||||||
- libnspr4
|
- libnspr4
|
||||||
- libnss3
|
- libnss3
|
||||||
- libpango1.0-0
|
- libpango1.0-0 | libpango-1.0-0
|
||||||
- libstdc++6
|
- libstdc++6
|
||||||
- libx11-6
|
- libx11-6
|
||||||
- libxcomposite1
|
- libxcomposite1
|
||||||
@@ -86,7 +87,11 @@ deb:
|
|||||||
- libxss1
|
- libxss1
|
||||||
- libxtst6
|
- libxtst6
|
||||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||||
|
afterInstall: "./after-install.tpl"
|
||||||
rpm:
|
rpm:
|
||||||
depends:
|
depends:
|
||||||
- lsb
|
- util-linux
|
||||||
- libXScrnSaver
|
protocols:
|
||||||
|
name: etcher
|
||||||
|
schemes:
|
||||||
|
- etcher
|
||||||
|
|||||||
18
entitlements.mac.plist
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.usb</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 2, June 1991
|
|
||||||
|
|
||||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
License is intended to guarantee your freedom to share and change free
|
|
||||||
software--to make sure the software is free for all its users. This
|
|
||||||
General Public License applies to most of the Free Software
|
|
||||||
Foundation's software and to any other program whose authors commit to
|
|
||||||
using it. (Some other Free Software Foundation software is covered by
|
|
||||||
the GNU Lesser General Public License instead.) You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
this service if you wish), that you receive source code or can get it
|
|
||||||
if you want it, that you can change the software or use pieces of it
|
|
||||||
in new free programs; and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to make restrictions that forbid
|
|
||||||
anyone to deny you these rights or to ask you to surrender the rights.
|
|
||||||
These restrictions translate to certain responsibilities for you if you
|
|
||||||
distribute copies of the software, or if you modify it.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must give the recipients all the rights that
|
|
||||||
you have. You must make sure that they, too, receive or can get the
|
|
||||||
source code. And you must show them these terms so they know their
|
|
||||||
rights.
|
|
||||||
|
|
||||||
We protect your rights with two steps: (1) copyright the software, and
|
|
||||||
(2) offer you this license which gives you legal permission to copy,
|
|
||||||
distribute and/or modify the software.
|
|
||||||
|
|
||||||
Also, for each author's protection and ours, we want to make certain
|
|
||||||
that everyone understands that there is no warranty for this free
|
|
||||||
software. If the software is modified by someone else and passed on, we
|
|
||||||
want its recipients to know that what they have is not the original, so
|
|
||||||
that any problems introduced by others will not reflect on the original
|
|
||||||
authors' reputations.
|
|
||||||
|
|
||||||
Finally, any free program is threatened constantly by software
|
|
||||||
patents. We wish to avoid the danger that redistributors of a free
|
|
||||||
program will individually obtain patent licenses, in effect making the
|
|
||||||
program proprietary. To prevent this, we have made it clear that any
|
|
||||||
patent must be licensed for everyone's free use or not licensed at all.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. This License applies to any program or other work which contains
|
|
||||||
a notice placed by the copyright holder saying it may be distributed
|
|
||||||
under the terms of this General Public License. The "Program", below,
|
|
||||||
refers to any such program or work, and a "work based on the Program"
|
|
||||||
means either the Program or any derivative work under copyright law:
|
|
||||||
that is to say, a work containing the Program or a portion of it,
|
|
||||||
either verbatim or with modifications and/or translated into another
|
|
||||||
language. (Hereinafter, translation is included without limitation in
|
|
||||||
the term "modification".) Each licensee is addressed as "you".
|
|
||||||
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running the Program is not restricted, and the output from the Program
|
|
||||||
is covered only if its contents constitute a work based on the
|
|
||||||
Program (independent of having been made by running the Program).
|
|
||||||
Whether that is true depends on what the Program does.
|
|
||||||
|
|
||||||
1. You may copy and distribute verbatim copies of the Program's
|
|
||||||
source code as you receive it, in any medium, provided that you
|
|
||||||
conspicuously and appropriately publish on each copy an appropriate
|
|
||||||
copyright notice and disclaimer of warranty; keep intact all the
|
|
||||||
notices that refer to this License and to the absence of any warranty;
|
|
||||||
and give any other recipients of the Program a copy of this License
|
|
||||||
along with the Program.
|
|
||||||
|
|
||||||
You may charge a fee for the physical act of transferring a copy, and
|
|
||||||
you may at your option offer warranty protection in exchange for a fee.
|
|
||||||
|
|
||||||
2. You may modify your copy or copies of the Program or any portion
|
|
||||||
of it, thus forming a work based on the Program, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) You must cause the modified files to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
|
|
||||||
b) You must cause any work that you distribute or publish, that in
|
|
||||||
whole or in part contains or is derived from the Program or any
|
|
||||||
part thereof, to be licensed as a whole at no charge to all third
|
|
||||||
parties under the terms of this License.
|
|
||||||
|
|
||||||
c) If the modified program normally reads commands interactively
|
|
||||||
when run, you must cause it, when started running for such
|
|
||||||
interactive use in the most ordinary way, to print or display an
|
|
||||||
announcement including an appropriate copyright notice and a
|
|
||||||
notice that there is no warranty (or else, saying that you provide
|
|
||||||
a warranty) and that users may redistribute the program under
|
|
||||||
these conditions, and telling the user how to view a copy of this
|
|
||||||
License. (Exception: if the Program itself is interactive but
|
|
||||||
does not normally print such an announcement, your work based on
|
|
||||||
the Program is not required to print an announcement.)
|
|
||||||
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Program,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Program, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote it.
|
|
||||||
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Program.
|
|
||||||
|
|
||||||
In addition, mere aggregation of another work not based on the Program
|
|
||||||
with the Program (or with a work based on the Program) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
|
|
||||||
3. You may copy and distribute the Program (or a work based on it,
|
|
||||||
under Section 2) in object code or executable form under the terms of
|
|
||||||
Sections 1 and 2 above provided that you also do one of the following:
|
|
||||||
|
|
||||||
a) Accompany it with the complete corresponding machine-readable
|
|
||||||
source code, which must be distributed under the terms of Sections
|
|
||||||
1 and 2 above on a medium customarily used for software interchange; or,
|
|
||||||
|
|
||||||
b) Accompany it with a written offer, valid for at least three
|
|
||||||
years, to give any third party, for a charge no more than your
|
|
||||||
cost of physically performing source distribution, a complete
|
|
||||||
machine-readable copy of the corresponding source code, to be
|
|
||||||
distributed under the terms of Sections 1 and 2 above on a medium
|
|
||||||
customarily used for software interchange; or,
|
|
||||||
|
|
||||||
c) Accompany it with the information you received as to the offer
|
|
||||||
to distribute corresponding source code. (This alternative is
|
|
||||||
allowed only for noncommercial distribution and only if you
|
|
||||||
received the program in object code or executable form with such
|
|
||||||
an offer, in accord with Subsection b above.)
|
|
||||||
|
|
||||||
The source code for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For an executable work, complete source
|
|
||||||
code means all the source code for all modules it contains, plus any
|
|
||||||
associated interface definition files, plus the scripts used to
|
|
||||||
control compilation and installation of the executable. However, as a
|
|
||||||
special exception, the source code distributed need not include
|
|
||||||
anything that is normally distributed (in either source or binary
|
|
||||||
form) with the major components (compiler, kernel, and so on) of the
|
|
||||||
operating system on which the executable runs, unless that component
|
|
||||||
itself accompanies the executable.
|
|
||||||
|
|
||||||
If distribution of executable or object code is made by offering
|
|
||||||
access to copy from a designated place, then offering equivalent
|
|
||||||
access to copy the source code from the same place counts as
|
|
||||||
distribution of the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
|
|
||||||
4. You may not copy, modify, sublicense, or distribute the Program
|
|
||||||
except as expressly provided under this License. Any attempt
|
|
||||||
otherwise to copy, modify, sublicense or distribute the Program is
|
|
||||||
void, and will automatically terminate your rights under this License.
|
|
||||||
However, parties who have received copies, or rights, from you under
|
|
||||||
this License will not have their licenses terminated so long as such
|
|
||||||
parties remain in full compliance.
|
|
||||||
|
|
||||||
5. You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Program or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Program (or any work based on the
|
|
||||||
Program), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Program or works based on it.
|
|
||||||
|
|
||||||
6. Each time you redistribute the Program (or any work based on the
|
|
||||||
Program), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute or modify the Program subject to
|
|
||||||
these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties to
|
|
||||||
this License.
|
|
||||||
|
|
||||||
7. If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Program at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Program by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Program.
|
|
||||||
|
|
||||||
If any portion of this section is held invalid or unenforceable under
|
|
||||||
any particular circumstance, the balance of the section is intended to
|
|
||||||
apply and the section as a whole is intended to apply in other
|
|
||||||
circumstances.
|
|
||||||
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system, which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
|
|
||||||
8. If the distribution and/or use of the Program is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Program under this License
|
|
||||||
may add an explicit geographical distribution limitation excluding
|
|
||||||
those countries, so that distribution is permitted only in or among
|
|
||||||
countries not thus excluded. In such case, this License incorporates
|
|
||||||
the limitation as if written in the body of this License.
|
|
||||||
|
|
||||||
9. The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
|
||||||
specifies a version number of this License which applies to it and "any
|
|
||||||
later version", you have the option of following the terms and conditions
|
|
||||||
either of that version or of any later version published by the Free
|
|
||||||
Software Foundation. If the Program does not specify a version number of
|
|
||||||
this License, you may choose any version ever published by the Free Software
|
|
||||||
Foundation.
|
|
||||||
|
|
||||||
10. If you wish to incorporate parts of the Program into other free
|
|
||||||
programs whose distribution conditions are different, write to the author
|
|
||||||
to ask for permission. For software which is copyrighted by the Free
|
|
||||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
|
||||||
make exceptions for this. Our decision will be guided by the two goals
|
|
||||||
of preserving the free status of all derivatives of our free software and
|
|
||||||
of promoting the sharing and reuse of software generally.
|
|
||||||
|
|
||||||
NO WARRANTY
|
|
||||||
|
|
||||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
|
||||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
|
||||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
|
||||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
|
||||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
|
||||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
|
||||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
|
||||||
REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
|
||||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
|
||||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
|
||||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
|
||||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
|
||||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGES.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
convey the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along
|
|
||||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program is interactive, make it output a short notice like this
|
|
||||||
when it starts in an interactive mode:
|
|
||||||
|
|
||||||
Gnomovision version 69, Copyright (C) year name of author
|
|
||||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, the commands you use may
|
|
||||||
be called something other than `show w' and `show c'; they could even be
|
|
||||||
mouse-clicks or menu items--whatever suits your program.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or your
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
|
||||||
necessary. Here is a sample; alter the names:
|
|
||||||
|
|
||||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
|
||||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
|
||||||
|
|
||||||
<signature of Ty Coon>, 1 April 1989
|
|
||||||
Ty Coon, President of Vice
|
|
||||||
|
|
||||||
This General Public License does not permit incorporating your program into
|
|
||||||
proprietary programs. If your program is a subroutine library, you may
|
|
||||||
consider it more useful to permit linking proprietary applications with the
|
|
||||||
library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
gpu_mem=16
|
|
||||||
dtoverlay=dwc2,dr_mode=peripheral
|
|
||||||
dtparam=act_led_trigger=none
|
|
||||||
dtparam=act_led_activelow=off
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
Copyright (c) 2006, Broadcom Corporation.
|
|
||||||
Copyright (c) 2015, Raspberry Pi (Trading) Ltd
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution. Redistribution and use in binary form, without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* This software may only be used for the purposes of developing for,
|
|
||||||
running or using a Raspberry Pi device.
|
|
||||||
* Redistributions must reproduce the above copyright notice and the
|
|
||||||
following disclaimer in the documentation and/or other materials
|
|
||||||
provided with the distribution.
|
|
||||||
* Neither the name of Broadcom Corporation nor the names of its suppliers
|
|
||||||
may be used to endorse or promote products derived from this software
|
|
||||||
without specific prior written permission.
|
|
||||||
|
|
||||||
DISCLAIMER. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
||||||
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
|
|
||||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
||||||
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
||||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
|
||||||
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
||||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
|
||||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
|
||||||
DAMAGE.
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
Etcher CLI
|
|
||||||
==========
|
|
||||||
|
|
||||||
The Etcher CLI is a command line interface to the Etcher writer backend, and
|
|
||||||
currently the only module in the "Etcher" umbrella that makes use of this
|
|
||||||
backend directly.
|
|
||||||
|
|
||||||
This module also has the task of unmounting the drives before and after
|
|
||||||
flashing.
|
|
||||||
|
|
||||||
Notice the Etcher CLI is not worried about elevation, and assumes it has enough
|
|
||||||
permissions to continue, throwing an error otherwise.
|
|
||||||
|
|
||||||
Exit codes
|
|
||||||
----------
|
|
||||||
|
|
||||||
The Etcher CLI uses certain exit codes to signal the result of the operation.
|
|
||||||
These are documented in [`lib/shared/exit-codes.js`][exit-codes] and are also
|
|
||||||
printed on the Etcher CLI help page.
|
|
||||||
|
|
||||||
[exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/shared/exit-codes.js
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const os = require('os')
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const crypto = require('crypto')
|
|
||||||
const childProcess = require('child_process')
|
|
||||||
const debug = require('debug')('etcher:cli:diskpart')
|
|
||||||
const Promise = require('bluebird')
|
|
||||||
const retry = require('bluebird-retry')
|
|
||||||
|
|
||||||
const TMP_RANDOM_BYTES = 6
|
|
||||||
const DISKPART_DELAY = 2000
|
|
||||||
const DISKPART_RETRIES = 5
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Generate a tmp filename with full path of OS' tmp dir
|
|
||||||
* @function
|
|
||||||
* @private
|
|
||||||
*
|
|
||||||
* @param {String} extension - temporary file extension
|
|
||||||
* @returns {String} filename
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const filename = tmpFilename('.sh');
|
|
||||||
*/
|
|
||||||
const tmpFilename = (extension) => {
|
|
||||||
const random = crypto.randomBytes(TMP_RANDOM_BYTES).toString('hex')
|
|
||||||
const filename = `etcher-diskpart-${random}${extension}`
|
|
||||||
return path.join(os.tmpdir(), filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Run a diskpart script
|
|
||||||
* @param {Array<String>} commands - list of commands to run
|
|
||||||
* @param {Function} callback - callback(error)
|
|
||||||
* @example
|
|
||||||
* runDiskpart(['rescan'], (error) => {
|
|
||||||
* ...
|
|
||||||
* })
|
|
||||||
*/
|
|
||||||
const runDiskpart = (commands, callback) => {
|
|
||||||
if (os.platform() !== 'win32') {
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const filename = tmpFilename('')
|
|
||||||
const script = commands.join('\r\n')
|
|
||||||
|
|
||||||
fs.writeFile(filename, script, {
|
|
||||||
mode: 0o755
|
|
||||||
}, (writeError) => {
|
|
||||||
debug('write %s:', filename, writeError || 'OK')
|
|
||||||
|
|
||||||
childProcess.exec(`diskpart /s ${filename}`, (execError, stdout, stderr) => {
|
|
||||||
debug('stdout:', stdout)
|
|
||||||
debug('stderr:', stderr)
|
|
||||||
|
|
||||||
fs.unlink(filename, (unlinkError) => {
|
|
||||||
debug('unlink %s:', filename, unlinkError || 'OK')
|
|
||||||
callback(execError)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Clean a device's partition tables
|
|
||||||
* @param {String} device - device path
|
|
||||||
* @example
|
|
||||||
* diskpart.clean('\\\\.\\PhysicalDrive2')
|
|
||||||
* .then(...)
|
|
||||||
* .catch(...)
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
clean (device) {
|
|
||||||
if (os.platform() !== 'win32') {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('clean', device)
|
|
||||||
|
|
||||||
const pattern = /PHYSICALDRIVE(\d+)/i
|
|
||||||
|
|
||||||
if (pattern.test(device)) {
|
|
||||||
const deviceId = device.match(pattern).pop()
|
|
||||||
return retry(() => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
runDiskpart([ `select disk ${deviceId}`, 'clean', 'rescan' ], (error) => {
|
|
||||||
return error ? reject(error) : resolve()
|
|
||||||
})
|
|
||||||
}).delay(DISKPART_DELAY)
|
|
||||||
}, {
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
max_tries: DISKPART_RETRIES
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
}).catch((error) => {
|
|
||||||
throw new Error(`Couldn't clean the drive, ${error.failure.message} (code ${error.failure.code})`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(new Error(`Invalid device: "${device}"`))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const Bluebird = require('bluebird')
|
|
||||||
const visuals = require('resin-cli-visuals')
|
|
||||||
const form = require('resin-cli-form')
|
|
||||||
const bytes = require('pretty-bytes')
|
|
||||||
const ImageWriter = require('../sdk/writer')
|
|
||||||
const utils = require('./utils')
|
|
||||||
const options = require('./options')
|
|
||||||
const messages = require('../shared/messages')
|
|
||||||
const EXIT_CODES = require('../shared/exit-codes')
|
|
||||||
const errors = require('../shared/errors')
|
|
||||||
const permissions = require('../shared/permissions')
|
|
||||||
|
|
||||||
/* eslint-disable no-magic-numbers */
|
|
||||||
|
|
||||||
const ARGV_IMAGE_PATH_INDEX = 0
|
|
||||||
const imagePath = options._[ARGV_IMAGE_PATH_INDEX]
|
|
||||||
|
|
||||||
permissions.isElevated().then((elevated) => {
|
|
||||||
if (!elevated) {
|
|
||||||
throw errors.createUserError({
|
|
||||||
title: messages.error.elevationRequired(),
|
|
||||||
description: 'This tool requires special permissions to write to external drives'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return form.run([
|
|
||||||
{
|
|
||||||
message: 'Select drive',
|
|
||||||
type: 'drive',
|
|
||||||
name: 'drive'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'This will erase the selected drive. Are you sure?',
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'yes',
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
], {
|
|
||||||
override: {
|
|
||||||
drive: options.drive,
|
|
||||||
|
|
||||||
// If `options.yes` is `false`, pass `null`,
|
|
||||||
// otherwise the question will not be asked because
|
|
||||||
// `false` is a defined value.
|
|
||||||
yes: options.yes || null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).then((answers) => {
|
|
||||||
if (!answers.yes) {
|
|
||||||
throw errors.createUserError({
|
|
||||||
title: 'Aborted',
|
|
||||||
description: 'We can\'t proceed without confirmation'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const progressBars = {
|
|
||||||
write: new visuals.Progress('Flashing'),
|
|
||||||
check: new visuals.Progress('Validating')
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Bluebird((resolve, reject) => {
|
|
||||||
/**
|
|
||||||
* @summary Progress update handler
|
|
||||||
* @param {Object} state - progress state
|
|
||||||
* @private
|
|
||||||
* @example
|
|
||||||
* writer.on('progress', onProgress)
|
|
||||||
*/
|
|
||||||
const onProgress = (state) => {
|
|
||||||
state.message = state.active > 1
|
|
||||||
? `${bytes(state.totalSpeed)}/s total, ${bytes(state.speed)}/s x ${state.active}`
|
|
||||||
: `${bytes(state.totalSpeed)}/s`
|
|
||||||
|
|
||||||
state.message = `${state.type === 'write' ? 'Flashing' : 'Validating'}: ${state.message}`
|
|
||||||
|
|
||||||
// Update progress bar
|
|
||||||
progressBars[state.type].update(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
const writer = new ImageWriter({
|
|
||||||
verify: options.check,
|
|
||||||
unmountOnSuccess: options.unmount,
|
|
||||||
checksumAlgorithms: options.check ? [ 'sha512' ] : []
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Finish handler
|
|
||||||
* @private
|
|
||||||
* @example
|
|
||||||
* writer.on('finish', onFinish)
|
|
||||||
*/
|
|
||||||
const onFinish = function () {
|
|
||||||
resolve(Array.from(writer.destinations.values()))
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.on('progress', onProgress)
|
|
||||||
writer.on('error', reject)
|
|
||||||
writer.on('finish', onFinish)
|
|
||||||
|
|
||||||
// NOTE: Drive can be (String|Array)
|
|
||||||
const destinations = [].concat(answers.drive)
|
|
||||||
|
|
||||||
writer.write(imagePath, destinations)
|
|
||||||
})
|
|
||||||
}).then((results) => {
|
|
||||||
let exitCode = EXIT_CODES.SUCCESS
|
|
||||||
|
|
||||||
if (options.check) {
|
|
||||||
console.log('')
|
|
||||||
console.log('Checksums:')
|
|
||||||
|
|
||||||
_.forEach(results, (result) => {
|
|
||||||
if (result.error) {
|
|
||||||
exitCode = EXIT_CODES.GENERAL_ERROR
|
|
||||||
console.log(` - ${result.device.device}: ${result.error.message}`)
|
|
||||||
} else {
|
|
||||||
console.log(` - ${result.device.device}: ${result.checksum.sha512}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(exitCode)
|
|
||||||
}).catch((error) => {
|
|
||||||
return Bluebird.try(() => {
|
|
||||||
utils.printError(error)
|
|
||||||
return Bluebird.resolve()
|
|
||||||
}).then(() => {
|
|
||||||
if (error.code === 'EVALIDATION') {
|
|
||||||
process.exit(EXIT_CODES.VALIDATION_ERROR)
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const fs = require('fs')
|
|
||||||
const yargs = require('yargs')
|
|
||||||
const utils = require('./utils')
|
|
||||||
const EXIT_CODES = require('../shared/exit-codes')
|
|
||||||
const errors = require('../shared/errors')
|
|
||||||
const packageJSON = require('../../package.json')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary The minimum required number of CLI arguments
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
* @type {Number}
|
|
||||||
*/
|
|
||||||
const MINIMUM_NUMBER_OF_ARGUMENTS = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary The index of the image argument
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
* @type {Number}
|
|
||||||
*/
|
|
||||||
const IMAGE_PATH_ARGV_INDEX = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary The first index that represents an actual option argument
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
* @type {Number}
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* The first arguments are usually the program executable itself, etc.
|
|
||||||
*/
|
|
||||||
const OPTIONS_INDEX_START = 2
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Parsed CLI options and arguments
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
module.exports = yargs
|
|
||||||
|
|
||||||
// Don't wrap at all
|
|
||||||
.wrap(null)
|
|
||||||
|
|
||||||
.demand(MINIMUM_NUMBER_OF_ARGUMENTS, 'Missing image')
|
|
||||||
|
|
||||||
// Usage help
|
|
||||||
.usage('Usage: $0 [options] <image>')
|
|
||||||
.epilogue([
|
|
||||||
'Exit codes:',
|
|
||||||
_.map(EXIT_CODES, (value, key) => {
|
|
||||||
const reason = _.map(_.split(key, '_'), _.capitalize).join(' ')
|
|
||||||
return ` ${value} - ${reason}`
|
|
||||||
}).join('\n'),
|
|
||||||
'',
|
|
||||||
'If you need help, don\'t hesitate in contacting us at:',
|
|
||||||
'',
|
|
||||||
' GitHub: https://github.com/resin-io/etcher/issues/new',
|
|
||||||
' Forums: https://forums.resin.io/c/etcher'
|
|
||||||
].join('\n'))
|
|
||||||
|
|
||||||
// Examples
|
|
||||||
.example('$0 raspberry-pi.img')
|
|
||||||
.example('$0 --no-check raspberry-pi.img')
|
|
||||||
.example('$0 -d /dev/disk2 ubuntu.iso')
|
|
||||||
.example('$0 -d /dev/disk2 -y rpi.img')
|
|
||||||
|
|
||||||
// Help option
|
|
||||||
.help()
|
|
||||||
|
|
||||||
// Version option
|
|
||||||
.version(packageJSON.version)
|
|
||||||
|
|
||||||
// Error reporting
|
|
||||||
.fail((message, error) => {
|
|
||||||
const errorObject = error || errors.createUserError({
|
|
||||||
title: message
|
|
||||||
})
|
|
||||||
|
|
||||||
yargs.showHelp()
|
|
||||||
utils.printError(errorObject)
|
|
||||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert that image exists
|
|
||||||
.check((argv) => {
|
|
||||||
const imagePath = argv._[IMAGE_PATH_ARGV_INDEX]
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.accessSync(imagePath)
|
|
||||||
} catch (error) {
|
|
||||||
throw errors.createUserError({
|
|
||||||
title: 'Unable to access file',
|
|
||||||
description: `The image ${imagePath} is not accessible`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert that if the `yes` flag is provided, the `drive` flag is also provided.
|
|
||||||
.check((argv) => {
|
|
||||||
if (argv.yes && !argv.drive) {
|
|
||||||
throw errors.createUserError({
|
|
||||||
title: 'Missing drive',
|
|
||||||
description: 'You need to explicitly pass a drive when disabling interactively'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
.options({
|
|
||||||
help: {
|
|
||||||
describe: 'show help',
|
|
||||||
boolean: true,
|
|
||||||
alias: 'h'
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
describe: 'show version number',
|
|
||||||
boolean: true,
|
|
||||||
alias: 'v'
|
|
||||||
},
|
|
||||||
drive: {
|
|
||||||
describe: 'drive',
|
|
||||||
string: true,
|
|
||||||
alias: 'd'
|
|
||||||
},
|
|
||||||
check: {
|
|
||||||
describe: 'validate write',
|
|
||||||
boolean: true,
|
|
||||||
alias: 'c',
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
yes: {
|
|
||||||
describe: 'confirm non-interactively',
|
|
||||||
boolean: true,
|
|
||||||
alias: 'y'
|
|
||||||
},
|
|
||||||
unmount: {
|
|
||||||
describe: 'unmount on success',
|
|
||||||
boolean: true,
|
|
||||||
alias: 'u',
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.parse(process.argv.slice(OPTIONS_INDEX_START))
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const chalk = require('chalk')
|
|
||||||
const errors = require('../shared/errors')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Print an error to stderr
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Error} error - error
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* utils.printError(new Error('Oops!'));
|
|
||||||
*/
|
|
||||||
exports.printError = (error) => {
|
|
||||||
const title = errors.getTitle(error)
|
|
||||||
const description = errors.getDescription(error, {
|
|
||||||
userFriendlyDescriptionsOnly: true
|
|
||||||
})
|
|
||||||
|
|
||||||
console.error(chalk.red(title))
|
|
||||||
|
|
||||||
if (description) {
|
|
||||||
console.error(`\n${chalk.red(description)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.ETCHER_CLI_DEBUG && error.stack) {
|
|
||||||
console.error(`\n${chalk.red(error.stack)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/* eslint-disable no-var */
|
|
||||||
|
|
||||||
var angular = require('angular')
|
|
||||||
|
|
||||||
/* eslint-enable no-var */
|
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const Bluebird = require('bluebird')
|
|
||||||
const semver = require('semver')
|
|
||||||
const EXIT_CODES = require('../../shared/exit-codes')
|
|
||||||
const messages = require('../../shared/messages')
|
|
||||||
const s3Packages = require('../../shared/s3-packages')
|
|
||||||
const release = require('../../shared/release')
|
|
||||||
const store = require('../../shared/store')
|
|
||||||
const errors = require('../../shared/errors')
|
|
||||||
const packageJSON = require('../../../package.json')
|
|
||||||
const flashState = require('../../shared/models/flash-state')
|
|
||||||
const settings = require('./models/settings')
|
|
||||||
const windowProgress = require('./os/window-progress')
|
|
||||||
const analytics = require('./modules/analytics')
|
|
||||||
const updateNotifier = require('./components/update-notifier')
|
|
||||||
const availableDrives = require('../../shared/models/available-drives')
|
|
||||||
const selectionState = require('../../shared/models/selection-state')
|
|
||||||
const driveScanner = require('./modules/drive-scanner')
|
|
||||||
const osDialog = require('./os/dialog')
|
|
||||||
const exceptionReporter = require('./modules/exception-reporter')
|
|
||||||
|
|
||||||
// Enable debug information from all modules that use `debug`
|
|
||||||
// See https://github.com/visionmedia/debug#browser-support
|
|
||||||
//
|
|
||||||
// Enable drivelist debugging information
|
|
||||||
// See https://github.com/resin-io-modules/drivelist
|
|
||||||
process.env.DRIVELIST_DEBUG = /drivelist|^\*$/i.test(process.env.DEBUG) ? '1' : ''
|
|
||||||
window.localStorage.debug = process.env.DEBUG
|
|
||||||
|
|
||||||
const app = angular.module('Etcher', [
|
|
||||||
require('angular-ui-router'),
|
|
||||||
require('angular-ui-bootstrap'),
|
|
||||||
require('angular-if-state'),
|
|
||||||
|
|
||||||
// Components
|
|
||||||
require('./components/svg-icon'),
|
|
||||||
require('./components/warning-modal/warning-modal'),
|
|
||||||
require('./components/safe-webview'),
|
|
||||||
|
|
||||||
// Pages
|
|
||||||
require('./pages/main/main'),
|
|
||||||
require('./pages/finish/finish'),
|
|
||||||
require('./pages/settings/settings'),
|
|
||||||
|
|
||||||
// OS
|
|
||||||
require('./os/open-external/open-external'),
|
|
||||||
require('./os/dropzone/dropzone'),
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
require('./utils/manifest-bind/manifest-bind')
|
|
||||||
])
|
|
||||||
|
|
||||||
app.run(() => {
|
|
||||||
console.log([
|
|
||||||
' _____ _ _',
|
|
||||||
'| ___| | | |',
|
|
||||||
'| |__ | |_ ___| |__ ___ _ __',
|
|
||||||
'| __|| __/ __| \'_ \\ / _ \\ \'__|',
|
|
||||||
'| |___| || (__| | | | __/ |',
|
|
||||||
'\\____/ \\__\\___|_| |_|\\___|_|',
|
|
||||||
'',
|
|
||||||
'Interested in joining the Etcher team?',
|
|
||||||
'Drop us a line at join+etcher@resin.io',
|
|
||||||
'',
|
|
||||||
`Version = ${packageJSON.version}, Type = ${packageJSON.packageType}`
|
|
||||||
].join('\n'))
|
|
||||||
})
|
|
||||||
|
|
||||||
app.run(() => {
|
|
||||||
const currentVersion = packageJSON.version
|
|
||||||
|
|
||||||
analytics.logEvent('Application start', {
|
|
||||||
packageType: packageJSON.packageType,
|
|
||||||
version: currentVersion
|
|
||||||
})
|
|
||||||
|
|
||||||
const shouldCheckForUpdates = updateNotifier.shouldCheckForUpdates({
|
|
||||||
currentVersion,
|
|
||||||
lastSleptUpdateNotifier: settings.get('lastSleptUpdateNotifier'),
|
|
||||||
lastSleptUpdateNotifierVersion: settings.get('lastSleptUpdateNotifierVersion')
|
|
||||||
})
|
|
||||||
|
|
||||||
const isStableRelease = release.isStableRelease(currentVersion)
|
|
||||||
const updatesEnabled = settings.get('updatesEnabled')
|
|
||||||
|
|
||||||
if (!shouldCheckForUpdates || !updatesEnabled) {
|
|
||||||
analytics.logEvent('Not checking for updates', {
|
|
||||||
shouldCheckForUpdates,
|
|
||||||
updatesEnabled,
|
|
||||||
stable: isStableRelease
|
|
||||||
})
|
|
||||||
|
|
||||||
return Bluebird.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSemverRange = packageJSON.updates.semverRange
|
|
||||||
const includeUnstableChannel = settings.get('includeUnstableUpdateChannel')
|
|
||||||
|
|
||||||
analytics.logEvent('Checking for updates', {
|
|
||||||
currentVersion,
|
|
||||||
stable: isStableRelease,
|
|
||||||
updateSemverRange,
|
|
||||||
includeUnstableChannel
|
|
||||||
})
|
|
||||||
|
|
||||||
return s3Packages.getLatestVersion(release.getReleaseType(currentVersion), {
|
|
||||||
range: updateSemverRange,
|
|
||||||
includeUnstableChannel
|
|
||||||
}).then((latestVersion) => {
|
|
||||||
if (semver.gte(currentVersion, latestVersion || '0.0.0')) {
|
|
||||||
analytics.logEvent('Update notification skipped', {
|
|
||||||
reason: 'Latest version'
|
|
||||||
})
|
|
||||||
return Bluebird.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case the internet connection is not good and checking the
|
|
||||||
// latest published version takes too long, only show notify
|
|
||||||
// the user about the new version if he didn't start the flash
|
|
||||||
// process (e.g: selected an image), otherwise such interruption
|
|
||||||
// might be annoying.
|
|
||||||
if (selectionState.hasImage()) {
|
|
||||||
analytics.logEvent('Update notification skipped', {
|
|
||||||
reason: 'Image selected'
|
|
||||||
})
|
|
||||||
return Bluebird.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
analytics.logEvent('Notifying update', {
|
|
||||||
latestVersion
|
|
||||||
})
|
|
||||||
|
|
||||||
return updateNotifier.notify(latestVersion, {
|
|
||||||
allowSleepUpdateCheck: isStableRelease
|
|
||||||
})
|
|
||||||
|
|
||||||
// If the error is an update user error, then we don't want
|
|
||||||
// to bother users each time they open the app.
|
|
||||||
// See: https://github.com/resin-io/etcher/issues/1525
|
|
||||||
}).catch((error) => {
|
|
||||||
return errors.isUserError(error) && error.code === 'UPDATE_USER_ERROR'
|
|
||||||
}, (error) => {
|
|
||||||
analytics.logEvent('Update check user error', {
|
|
||||||
title: errors.getTitle(error),
|
|
||||||
description: errors.getDescription(error)
|
|
||||||
})
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.run(() => {
|
|
||||||
store.subscribe(() => {
|
|
||||||
if (!flashState.isFlashing()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentFlashState = flashState.getFlashState()
|
|
||||||
const stateType = !currentFlashState.flashing && currentFlashState.verifying
|
|
||||||
? `Verifying ${currentFlashState.verifying}`
|
|
||||||
: `Flashing ${currentFlashState.flashing}`
|
|
||||||
|
|
||||||
// NOTE: There is usually a short time period between the `isFlashing()`
|
|
||||||
// property being set, and the flashing actually starting, which
|
|
||||||
// might cause some non-sense flashing state logs including
|
|
||||||
// `undefined` values.
|
|
||||||
analytics.logDebug(
|
|
||||||
`${stateType} devices, ` +
|
|
||||||
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` +
|
|
||||||
`(total ${currentFlashState.totalSpeed} MB/s) ` +
|
|
||||||
`eta in ${currentFlashState.eta}s ` +
|
|
||||||
`with ${currentFlashState.failed} failed devices`
|
|
||||||
)
|
|
||||||
|
|
||||||
windowProgress.set(currentFlashState)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.run(($timeout) => {
|
|
||||||
driveScanner.on('devices', (drives) => {
|
|
||||||
// Safely trigger a digest cycle.
|
|
||||||
// In some cases, AngularJS doesn't acknowledge that the
|
|
||||||
// available drives list has changed, and incorrectly
|
|
||||||
// keeps asking the user to "Connect a drive".
|
|
||||||
$timeout(() => {
|
|
||||||
availableDrives.setDrives(drives)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
driveScanner.on('error', (error) => {
|
|
||||||
// Stop the drive scanning loop in case of errors,
|
|
||||||
// otherwise we risk presenting the same error over
|
|
||||||
// and over again to the user, while also heavily
|
|
||||||
// spamming our error reporting service.
|
|
||||||
driveScanner.stop()
|
|
||||||
|
|
||||||
return exceptionReporter.report(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
driveScanner.start()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.run(($window) => {
|
|
||||||
let popupExists = false
|
|
||||||
|
|
||||||
$window.addEventListener('beforeunload', (event) => {
|
|
||||||
if (!flashState.isFlashing() || popupExists) {
|
|
||||||
analytics.logEvent('Close application', {
|
|
||||||
isFlashing: flashState.isFlashing()
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't close window while flashing
|
|
||||||
event.returnValue = false
|
|
||||||
|
|
||||||
// Don't open any more popups
|
|
||||||
popupExists = true
|
|
||||||
|
|
||||||
analytics.logEvent('Close attempt while flashing')
|
|
||||||
|
|
||||||
osDialog.showWarning({
|
|
||||||
confirmationLabel: 'Yes, quit',
|
|
||||||
rejectionLabel: 'Cancel',
|
|
||||||
title: 'Are you sure you want to close Etcher?',
|
|
||||||
description: messages.warning.exitWhileFlashing()
|
|
||||||
}).then((confirmed) => {
|
|
||||||
if (confirmed) {
|
|
||||||
analytics.logEvent('Close confirmed while flashing', {
|
|
||||||
uuid: flashState.getFlashUuid()
|
|
||||||
})
|
|
||||||
|
|
||||||
// This circumvents the 'beforeunload' event unlike
|
|
||||||
// electron.remote.app.quit() which does not.
|
|
||||||
electron.remote.process.exit(EXIT_CODES.SUCCESS)
|
|
||||||
}
|
|
||||||
|
|
||||||
analytics.logEvent('Close rejected while flashing')
|
|
||||||
popupExists = false
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.run(($rootScope) => {
|
|
||||||
$rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
|
||||||
// Ignore first navigation
|
|
||||||
if (!fromState.name) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
analytics.logEvent('Navigate', {
|
|
||||||
to: toState.name,
|
|
||||||
from: fromState.name
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.config(($urlRouterProvider) => {
|
|
||||||
$urlRouterProvider.otherwise('/main')
|
|
||||||
})
|
|
||||||
|
|
||||||
app.config(($provide) => {
|
|
||||||
$provide.decorator('$exceptionHandler', ($delegate) => {
|
|
||||||
return (exception, cause) => {
|
|
||||||
exceptionReporter.report(exception)
|
|
||||||
$delegate(exception, cause)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.controller('HeaderController', function (OSOpenExternalService) {
|
|
||||||
/**
|
|
||||||
* @summary Open help page
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This application will open either the image's support url, declared
|
|
||||||
* in the archive `manifest.json`, or the default Etcher help page.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* HeaderController.openHelpPage();
|
|
||||||
*/
|
|
||||||
this.openHelpPage = () => {
|
|
||||||
const DEFAULT_SUPPORT_URL = 'https://github.com/resin-io/etcher/blob/master/SUPPORT.md'
|
|
||||||
const supportUrl = selectionState.getImageSupportUrl() || DEFAULT_SUPPORT_URL
|
|
||||||
OSOpenExternalService.open(supportUrl)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.controller('StateController', function ($rootScope, $scope) {
|
|
||||||
const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
|
||||||
this.previousName = fromState.name
|
|
||||||
this.currentName = toState.name
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.$on('$destroy', unregisterStateChange)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get the previous state name
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} previous state name
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* if (StateController.previousName === 'main') {
|
|
||||||
* console.log('We left the main screen!');
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this.previousName = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get the current state name
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} current state name
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* if (StateController.currentName === 'main') {
|
|
||||||
* console.log('We are on the main screen!');
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this.currentName = null
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle keyboard shortcut to open the settings
|
|
||||||
app.run(($state) => {
|
|
||||||
electron.ipcRenderer.on('menu:preferences', () => {
|
|
||||||
$state.go('settings')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ensure user settings are loaded before
|
|
||||||
// we bootstrap the Angular.js application
|
|
||||||
angular.element(document).ready(() => {
|
|
||||||
settings.load().then(() => {
|
|
||||||
angular.bootstrap(document, [ 'Etcher' ])
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
})
|
|
||||||
363
lib/gui/app/app.ts
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
/*
|
||||||
|
* 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 electron from 'electron';
|
||||||
|
import * as sdk from 'etcher-sdk';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import outdent from 'outdent';
|
||||||
|
import * as React from 'react';
|
||||||
|
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 { deselectImage, getImage } from './models/selection-state';
|
||||||
|
import * as settings from './models/settings';
|
||||||
|
import { Actions, observe, store } from './models/store';
|
||||||
|
import * as analytics from './modules/analytics';
|
||||||
|
import { scanner as driveScanner } from './modules/drive-scanner';
|
||||||
|
import * as exceptionReporter from './modules/exception-reporter';
|
||||||
|
import * as osDialog from './os/dialog';
|
||||||
|
import * as windowProgress from './os/window-progress';
|
||||||
|
import MainPage from './pages/main/MainPage';
|
||||||
|
import './css/main.css';
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
'unhandledrejection',
|
||||||
|
(event: PromiseRejectionEvent | any) => {
|
||||||
|
// Promise: event.reason
|
||||||
|
// Anything else: event
|
||||||
|
const error = event.reason || event;
|
||||||
|
analytics.logException(error);
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set application session UUID
|
||||||
|
store.dispatch({
|
||||||
|
type: Actions.SET_APPLICATION_SESSION_UUID,
|
||||||
|
data: uuidV4(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set first flashing workflow UUID
|
||||||
|
store.dispatch({
|
||||||
|
type: Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||||
|
data: uuidV4(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const applicationSessionUuid = store.getState().toJS().applicationSessionUuid;
|
||||||
|
const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid;
|
||||||
|
|
||||||
|
console.log(outdent`
|
||||||
|
${outdent}
|
||||||
|
_____ _ _
|
||||||
|
| ___| | | |
|
||||||
|
| |__ | |_ ___| |__ ___ _ __
|
||||||
|
| __|| __/ __| '_ \\ / _ \\ '__|
|
||||||
|
| |___| || (__| | | | __/ |
|
||||||
|
\\____/ \\__\\___|_| |_|\\___|_|
|
||||||
|
|
||||||
|
Interested in joining the Etcher team?
|
||||||
|
Drop us a line at join+etcher@balena.io
|
||||||
|
|
||||||
|
Version = ${packageJSON.version}, Type = ${packageJSON.packageType}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const currentVersion = packageJSON.version;
|
||||||
|
|
||||||
|
analytics.logEvent('Application start', {
|
||||||
|
packageType: packageJSON.packageType,
|
||||||
|
version: currentVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
const debouncedLog = _.debounce(console.log, 1000, { maxWait: 1000 });
|
||||||
|
|
||||||
|
function pluralize(word: string, quantity: number) {
|
||||||
|
return `${quantity} ${word}${quantity === 1 ? '' : 's'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
observe(() => {
|
||||||
|
if (!flashState.isFlashing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentFlashState = flashState.getFlashState();
|
||||||
|
windowProgress.set(currentFlashState);
|
||||||
|
|
||||||
|
let eta = '';
|
||||||
|
if (currentFlashState.eta !== undefined) {
|
||||||
|
eta = `eta in ${currentFlashState.eta.toFixed(0)}s`;
|
||||||
|
}
|
||||||
|
let active = '';
|
||||||
|
if (currentFlashState.type !== 'decompressing') {
|
||||||
|
active = pluralize('device', currentFlashState.active);
|
||||||
|
}
|
||||||
|
// NOTE: There is usually a short time period between the `isFlashing()`
|
||||||
|
// property being set, and the flashing actually starting, which
|
||||||
|
// might cause some non-sense flashing state logs including
|
||||||
|
// `undefined` values.
|
||||||
|
debouncedLog(outdent({ newline: ' ' })`
|
||||||
|
${_.capitalize(currentFlashState.type)}
|
||||||
|
${active},
|
||||||
|
${currentFlashState.percentage}%
|
||||||
|
at
|
||||||
|
${(currentFlashState.speed || 0).toFixed(2)}
|
||||||
|
MB/s
|
||||||
|
(total ${(currentFlashState.speed * currentFlashState.active).toFixed(2)} MB/s)
|
||||||
|
${eta}
|
||||||
|
with
|
||||||
|
${pluralize('failed device', currentFlashState.failed)}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary The radix used by USB ID numbers
|
||||||
|
*/
|
||||||
|
const USB_ID_RADIX = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary The expected length of a USB ID number
|
||||||
|
*/
|
||||||
|
const USB_ID_LENGTH = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Convert a USB id (e.g. product/vendor) to a string
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* console.log(usbIdToString(2652))
|
||||||
|
* > '0x0a5c'
|
||||||
|
*/
|
||||||
|
function usbIdToString(id: number): string {
|
||||||
|
return `0x${_.padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Product ID of BCM2708
|
||||||
|
*/
|
||||||
|
const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Product ID of BCM2710
|
||||||
|
*/
|
||||||
|
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Compute module descriptions
|
||||||
|
*/
|
||||||
|
const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary<string> = {
|
||||||
|
[USB_PRODUCT_ID_BCM2708_BOOT]: 'Compute Module 1',
|
||||||
|
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function driveIsAllowed(drive: {
|
||||||
|
devicePath: string;
|
||||||
|
device: string;
|
||||||
|
raw: string;
|
||||||
|
}) {
|
||||||
|
const driveBlacklist = (await settings.get('driveBlacklist')) || [];
|
||||||
|
return !(
|
||||||
|
driveBlacklist.includes(drive.devicePath) ||
|
||||||
|
driveBlacklist.includes(drive.device) ||
|
||||||
|
driveBlacklist.includes(drive.raw)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Drive =
|
||||||
|
| sdk.sourceDestination.BlockDevice
|
||||||
|
| sdk.sourceDestination.UsbbootDrive
|
||||||
|
| sdk.sourceDestination.DriverlessDevice;
|
||||||
|
|
||||||
|
function prepareDrive(drive: Drive) {
|
||||||
|
if (drive instanceof sdk.sourceDestination.BlockDevice) {
|
||||||
|
// @ts-ignore (BlockDevice.drive is private)
|
||||||
|
return drive.drive;
|
||||||
|
} else if (drive instanceof sdk.sourceDestination.UsbbootDrive) {
|
||||||
|
// This is a workaround etcher expecting a device string and a size
|
||||||
|
// @ts-ignore
|
||||||
|
drive.device = drive.usbDevice.portId;
|
||||||
|
drive.size = null;
|
||||||
|
// @ts-ignore
|
||||||
|
drive.progress = 0;
|
||||||
|
drive.disabled = true;
|
||||||
|
drive.on('progress', (progress) => {
|
||||||
|
updateDriveProgress(drive, progress);
|
||||||
|
});
|
||||||
|
return drive;
|
||||||
|
} else if (drive instanceof sdk.sourceDestination.DriverlessDevice) {
|
||||||
|
const description =
|
||||||
|
COMPUTE_MODULE_DESCRIPTIONS[
|
||||||
|
drive.deviceDescriptor.idProduct.toString()
|
||||||
|
] || 'Compute Module';
|
||||||
|
return {
|
||||||
|
device: `${usbIdToString(
|
||||||
|
drive.deviceDescriptor.idVendor,
|
||||||
|
)}:${usbIdToString(drive.deviceDescriptor.idProduct)}`,
|
||||||
|
displayName: 'Missing drivers',
|
||||||
|
description,
|
||||||
|
mountpoints: [],
|
||||||
|
isReadOnly: false,
|
||||||
|
isSystem: false,
|
||||||
|
disabled: true,
|
||||||
|
icon: 'warning',
|
||||||
|
size: null,
|
||||||
|
link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
|
||||||
|
linkCTA: 'Install',
|
||||||
|
linkTitle: 'Install missing drivers',
|
||||||
|
linkMessage: outdent`
|
||||||
|
Would you like to download the necessary drivers from the Raspberry Pi Foundation?
|
||||||
|
This will open your browser.
|
||||||
|
|
||||||
|
|
||||||
|
Once opened, download and run the installer from the "Windows Installer" section to install the drivers
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
|
||||||
|
availableDrives.setDrives(_.values(drives));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDrives() {
|
||||||
|
return _.keyBy(availableDrives.getDrives(), 'device');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addDrive(drive: Drive) {
|
||||||
|
const preparedDrive = prepareDrive(drive);
|
||||||
|
if (!(await driveIsAllowed(preparedDrive))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const drives = getDrives();
|
||||||
|
drives[preparedDrive.device] = preparedDrive;
|
||||||
|
setDrives(drives);
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
setDrives(drives);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDriveProgress(
|
||||||
|
drive: sdk.sourceDestination.UsbbootDrive,
|
||||||
|
progress: number,
|
||||||
|
) {
|
||||||
|
const drives = getDrives();
|
||||||
|
// @ts-ignore
|
||||||
|
const driveInMap = drives[drive.device];
|
||||||
|
if (driveInMap) {
|
||||||
|
// @ts-ignore
|
||||||
|
drives[drive.device] = { ...driveInMap, progress };
|
||||||
|
setDrives(drives);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
driveScanner.on('attach', addDrive);
|
||||||
|
driveScanner.on('detach', removeDrive);
|
||||||
|
|
||||||
|
driveScanner.on('error', (error) => {
|
||||||
|
// Stop the drive scanning loop in case of errors,
|
||||||
|
// otherwise we risk presenting the same error over
|
||||||
|
// and over again to the user, while also heavily
|
||||||
|
// spamming our error reporting service.
|
||||||
|
driveScanner.stop();
|
||||||
|
|
||||||
|
return exceptionReporter.report(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
driveScanner.start();
|
||||||
|
|
||||||
|
let popupExists = false;
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', async (event) => {
|
||||||
|
if (!flashState.isFlashing() || popupExists) {
|
||||||
|
analytics.logEvent('Close application', {
|
||||||
|
isFlashing: flashState.isFlashing(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't close window while flashing
|
||||||
|
event.returnValue = false;
|
||||||
|
|
||||||
|
// Don't open any more popups
|
||||||
|
popupExists = true;
|
||||||
|
|
||||||
|
analytics.logEvent('Close attempt while flashing');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const confirmed = await osDialog.showWarning({
|
||||||
|
confirmationLabel: 'Yes, quit',
|
||||||
|
rejectionLabel: 'Cancel',
|
||||||
|
title: 'Are you sure you want to close Etcher?',
|
||||||
|
description: messages.warning.exitWhileFlashing(),
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
analytics.logEvent('Close confirmed while flashing', {
|
||||||
|
flashInstanceUuid: flashState.getFlashUuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// This circumvents the 'beforeunload' event unlike
|
||||||
|
// electron.remote.app.quit() which does not.
|
||||||
|
electron.remote.process.exit(EXIT_CODES.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
analytics.logEvent('Close rejected while flashing', {
|
||||||
|
applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
popupExists = false;
|
||||||
|
} catch (error: any) {
|
||||||
|
exceptionReporter.report(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const _ = require('lodash')
|
|
||||||
const constraints = require('../../../../../shared/drive-constraints')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
const availableDrives = require('../../../../../shared/models/available-drives')
|
|
||||||
const selectionState = require('../../../../../shared/models/selection-state')
|
|
||||||
const utils = require('../../../../../shared/utils')
|
|
||||||
|
|
||||||
module.exports = function (
|
|
||||||
$q,
|
|
||||||
$uibModalInstance
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* @summary The drive selector state
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.state = selectionState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Static methods to check a drive's properties
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.constraints = constraints
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary The drives model
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* We expose the whole service instead of the `.drives`
|
|
||||||
* property, which is the one we're interested in since
|
|
||||||
* this allows the property to be automatically updated
|
|
||||||
* when `availableDrives` detects a change in the drives.
|
|
||||||
*/
|
|
||||||
this.drives = availableDrives
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Determine if we can change a drive's selection state
|
|
||||||
* @function
|
|
||||||
* @private
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.shouldChangeDriveSelectionState(drive)
|
|
||||||
* .then((shouldChangeDriveSelectionState) => {
|
|
||||||
* if (shouldChangeDriveSelectionState) doSomething();
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
const shouldChangeDriveSelectionState = (drive) => {
|
|
||||||
return $q.resolve(constraints.isDriveValid(drive, selectionState.getImage()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Toggle a drive selection
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise} - resolved promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.toggleDrive({
|
|
||||||
* device: '/dev/disk2',
|
|
||||||
* size: 999999999,
|
|
||||||
* name: 'Cruzer USB drive'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.toggleDrive = (drive) => {
|
|
||||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
|
||||||
if (canChangeDriveSelectionState) {
|
|
||||||
analytics.logEvent('Toggle drive', {
|
|
||||||
drive,
|
|
||||||
previouslySelected: selectionState.isCurrentDrive(drive.device)
|
|
||||||
})
|
|
||||||
|
|
||||||
selectionState.toggleDrive(drive.device)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Close the modal and resolve the selected drive
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.closeModal();
|
|
||||||
*/
|
|
||||||
this.closeModal = () => {
|
|
||||||
const selectedDrive = selectionState.getCurrentDrive()
|
|
||||||
|
|
||||||
// Sanity check to cover the case where a drive is selected,
|
|
||||||
// the drive is then unplugged from the computer and the modal
|
|
||||||
// is resolved with a non-existent drive.
|
|
||||||
if (!selectedDrive || !_.includes(this.drives.getDrives(), selectedDrive)) {
|
|
||||||
$uibModalInstance.close()
|
|
||||||
} else {
|
|
||||||
$uibModalInstance.close(selectedDrive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Select a drive and close the modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise} - resolved promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.selectDriveAndClose({
|
|
||||||
* device: '/dev/disk2',
|
|
||||||
* size: 999999999,
|
|
||||||
* name: 'Cruzer USB drive'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.selectDriveAndClose = (drive) => {
|
|
||||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
|
||||||
if (canChangeDriveSelectionState) {
|
|
||||||
selectionState.selectDrive(drive.device)
|
|
||||||
|
|
||||||
analytics.logEvent('Drive selected (double click)')
|
|
||||||
|
|
||||||
this.closeModal()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Memoized getDrives function
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {Array<Object>} - memoized list of drives
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const drives = DriveSelectorController.getDrives()
|
|
||||||
* // Do something with drives
|
|
||||||
*/
|
|
||||||
this.getDrives = utils.memoize(this.drives.getDrives, angular.equals)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get a drive's compatibility status object(s)
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Given a drive, return its compatibility status with the selected image,
|
|
||||||
* containing the status type (ERROR, WARNING), and accompanying
|
|
||||||
* status message.
|
|
||||||
*
|
|
||||||
* @returns {Object[]} list of objects containing statuses
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const statuses = DriveSelectorController.getDriveStatuses(drive);
|
|
||||||
*
|
|
||||||
* for ({ type, message } of statuses) {
|
|
||||||
* // do something
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this.getDriveStatuses = utils.memoize((drive) => {
|
|
||||||
return this.constraints.getDriveImageCompatibilityStatuses(drive, this.state.getImage())
|
|
||||||
}, angular.equals)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Keyboard event drive toggling
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Keyboard-event specific entry to the toggleDrive function.
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @param {Object} $event - event
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <div tabindex="1" ng-keypress="this.keyboardToggleDrive(drive, $event)">
|
|
||||||
* Tab-select me and press enter or space!
|
|
||||||
* </div>
|
|
||||||
*/
|
|
||||||
this.keyboardToggleDrive = (drive, $event) => {
|
|
||||||
console.log($event.keyCode)
|
|
||||||
const ENTER = 13
|
|
||||||
const SPACE = 32
|
|
||||||
if (_.includes([ ENTER, SPACE ], $event.keyCode)) {
|
|
||||||
this.toggleDrive(drive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.DriveSelector
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Components.DriveSelector'
|
|
||||||
const DriveSelector = angular.module(MODULE_NAME, [
|
|
||||||
require('../modal/modal'),
|
|
||||||
require('../../utils/byte-size/byte-size')
|
|
||||||
])
|
|
||||||
|
|
||||||
DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector'))
|
|
||||||
DriveSelector.service('DriveSelectorService', require('./services/drive-selector'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
||||||
556
lib/gui/app/components/drive-selector/drive-selector.tsx
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||||
|
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||||
|
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDriveImageCompatibilityStatuses,
|
||||||
|
isDriveValid,
|
||||||
|
DriveStatus,
|
||||||
|
DrivelistDrive,
|
||||||
|
isDriveSizeLarge,
|
||||||
|
} from '../../../../shared/drive-constraints';
|
||||||
|
import { compatibility, warning } from '../../../../shared/messages';
|
||||||
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
|
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||||
|
import { getImage, isDriveSelected } from '../../models/selection-state';
|
||||||
|
import { store } from '../../models/store';
|
||||||
|
import { logEvent, logException } from '../../modules/analytics';
|
||||||
|
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
GenericTableProps,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
} from '../../styled-components';
|
||||||
|
|
||||||
|
import { SourceMetadata } from '../source-selector/source-selector';
|
||||||
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
|
||||||
|
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||||
|
progress: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DriverlessDrive {
|
||||||
|
displayName: string; // added in app.ts
|
||||||
|
description: string;
|
||||||
|
link: string;
|
||||||
|
linkTitle: string;
|
||||||
|
linkMessage: string;
|
||||||
|
linkCTA: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Drive = DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||||
|
|
||||||
|
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
|
||||||
|
return (drive as UsbbootDrive).progress !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
|
||||||
|
return (drive as DriverlessDrive).link !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
|
||||||
|
return typeof (drive as DrivelistDrive).size === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
const DrivesTable = styled((props: GenericTableProps<Drive>) => (
|
||||||
|
<Table<Drive> {...props} />
|
||||||
|
))`
|
||||||
|
[data-display='table-head'],
|
||||||
|
[data-display='table-body'] {
|
||||||
|
> [data-display='table-row'] > [data-display='table-cell'] {
|
||||||
|
&:nth-child(2) {
|
||||||
|
width: 32%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(5) {
|
||||||
|
width: 32%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function badgeShadeFromStatus(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case compatibility.containsImage():
|
||||||
|
return 16;
|
||||||
|
case compatibility.system():
|
||||||
|
case compatibility.tooSmall():
|
||||||
|
return 5;
|
||||||
|
default:
|
||||||
|
return 14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const InitProgress = styled(
|
||||||
|
({
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
props?: React.ProgressHTMLAttributes<Element>;
|
||||||
|
}) => {
|
||||||
|
return <progress max="100" value={value} {...props} />;
|
||||||
|
},
|
||||||
|
)`
|
||||||
|
/* Reset the default appearance */
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
::-webkit-progress-bar {
|
||||||
|
width: 130px;
|
||||||
|
height: 4px;
|
||||||
|
background-color: #dde1f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-progress-value {
|
||||||
|
background-color: #1496e1;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface DriveSelectorProps
|
||||||
|
extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
|
||||||
|
write: boolean;
|
||||||
|
multipleSelection: boolean;
|
||||||
|
showWarnings?: boolean;
|
||||||
|
cancel: (drives: DrivelistDrive[]) => void;
|
||||||
|
done: (drives: DrivelistDrive[]) => void;
|
||||||
|
titleLabel: string;
|
||||||
|
emptyListLabel: string;
|
||||||
|
emptyListIcon: JSX.Element;
|
||||||
|
selectedList?: DrivelistDrive[];
|
||||||
|
updateSelectedList?: () => DrivelistDrive[];
|
||||||
|
onSelect?: (drive: DrivelistDrive) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DriveSelectorState {
|
||||||
|
drives: Drive[];
|
||||||
|
image?: SourceMetadata;
|
||||||
|
missingDriversModal: { drive?: DriverlessDrive };
|
||||||
|
selectedList: DrivelistDrive[];
|
||||||
|
showSystemDrives: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSystemDrive(drive: Drive) {
|
||||||
|
return isDrivelistDrive(drive) && drive.isSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DriveSelector extends React.Component<
|
||||||
|
DriveSelectorProps,
|
||||||
|
DriveSelectorState
|
||||||
|
> {
|
||||||
|
private unsubscribe: (() => void) | undefined;
|
||||||
|
tableColumns: Array<TableColumn<Drive>>;
|
||||||
|
originalList: DrivelistDrive[];
|
||||||
|
|
||||||
|
constructor(props: DriveSelectorProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||||
|
const selectedList = this.props.selectedList || [];
|
||||||
|
this.originalList = [...(this.props.selectedList || [])];
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
drives: getDrives(),
|
||||||
|
image: getImage(),
|
||||||
|
missingDriversModal: defaultMissingDriversModalState,
|
||||||
|
selectedList,
|
||||||
|
showSystemDrives: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tableColumns = [
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
label: 'Name',
|
||||||
|
render: (description: string, drive: Drive) => {
|
||||||
|
if (isDrivelistDrive(drive)) {
|
||||||
|
const isLargeDrive = isDriveSizeLarge(drive);
|
||||||
|
const hasWarnings =
|
||||||
|
this.props.showWarnings && (isLargeDrive || drive.isSystem);
|
||||||
|
return (
|
||||||
|
<Flex alignItems="center">
|
||||||
|
{hasWarnings && (
|
||||||
|
<ExclamationTriangleSvg
|
||||||
|
height="1em"
|
||||||
|
fill={drive.isSystem ? '#fca321' : '#8f9297'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Txt ml={(hasWarnings && 8) || 0}>
|
||||||
|
{middleEllipsis(description, 32)}
|
||||||
|
</Txt>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Txt>{description}</Txt>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
key: 'size',
|
||||||
|
label: 'Size',
|
||||||
|
render: (_description: string, drive: Drive) => {
|
||||||
|
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||||
|
return prettyBytes(drive.size);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
key: 'link',
|
||||||
|
label: 'Location',
|
||||||
|
render: (_description: string, drive: Drive) => {
|
||||||
|
return (
|
||||||
|
<Txt>
|
||||||
|
{drive.displayName}
|
||||||
|
{isDriverlessDrive(drive) && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
-{' '}
|
||||||
|
<b>
|
||||||
|
<a onClick={() => this.installMissingDrivers(drive)}>
|
||||||
|
{drive.linkCTA}
|
||||||
|
</a>
|
||||||
|
</b>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Txt>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
key: 'extra',
|
||||||
|
// We use an empty React fragment otherwise it uses the field name as label
|
||||||
|
label: <></>,
|
||||||
|
render: (_description: string, drive: Drive) => {
|
||||||
|
if (isUsbbootDrive(drive)) {
|
||||||
|
return this.renderProgress(drive.progress);
|
||||||
|
} else if (isDrivelistDrive(drive)) {
|
||||||
|
return this.renderStatuses(drive);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private driveShouldBeDisabled(drive: Drive, image?: SourceMetadata) {
|
||||||
|
return (
|
||||||
|
isUsbbootDrive(drive) ||
|
||||||
|
isDriverlessDrive(drive) ||
|
||||||
|
!isDriveValid(drive, image, this.props.write) ||
|
||||||
|
(this.props.write && drive.isReadOnly)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDisplayedDrives(drives: Drive[]): Drive[] {
|
||||||
|
return drives.filter((drive) => {
|
||||||
|
return (
|
||||||
|
isUsbbootDrive(drive) ||
|
||||||
|
isDriverlessDrive(drive) ||
|
||||||
|
isDriveSelected(drive.device) ||
|
||||||
|
this.state.showSystemDrives ||
|
||||||
|
!drive.isSystem
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDisabledDrives(drives: Drive[], image?: SourceMetadata): string[] {
|
||||||
|
return drives
|
||||||
|
.filter((drive) => this.driveShouldBeDisabled(drive, image))
|
||||||
|
.map((drive) => drive.displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderProgress(progress: number) {
|
||||||
|
return (
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<Txt fontSize={12}>Initializing device</Txt>
|
||||||
|
<InitProgress value={progress} />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private warningFromStatus(
|
||||||
|
status: string,
|
||||||
|
drive: { device: string; size: number },
|
||||||
|
) {
|
||||||
|
switch (status) {
|
||||||
|
case compatibility.containsImage():
|
||||||
|
return warning.sourceDrive();
|
||||||
|
case compatibility.largeDrive():
|
||||||
|
return warning.largeDriveSize();
|
||||||
|
case compatibility.system():
|
||||||
|
return warning.systemDrive();
|
||||||
|
case compatibility.tooSmall():
|
||||||
|
const size =
|
||||||
|
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
||||||
|
return warning.tooSmall({ size }, drive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderStatuses(drive: DrivelistDrive) {
|
||||||
|
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
|
||||||
|
drive,
|
||||||
|
this.state.image,
|
||||||
|
this.props.write,
|
||||||
|
).slice(0, 2);
|
||||||
|
return (
|
||||||
|
// the column render fn expects a single Element
|
||||||
|
<>
|
||||||
|
{statuses.map((status) => {
|
||||||
|
const badgeShade = badgeShadeFromStatus(status.message);
|
||||||
|
const warningMessage = this.warningFromStatus(status.message, {
|
||||||
|
device: drive.device,
|
||||||
|
size: drive.size || 0,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={status.message}
|
||||||
|
shade={badgeShade}
|
||||||
|
mr="8px"
|
||||||
|
tooltip={this.props.showWarnings ? warningMessage : ''}
|
||||||
|
>
|
||||||
|
{status.message}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private installMissingDrivers(drive: DriverlessDrive) {
|
||||||
|
if (drive.link) {
|
||||||
|
logEvent('Open driver link modal', {
|
||||||
|
url: drive.link,
|
||||||
|
});
|
||||||
|
this.setState({ missingDriversModal: { drive } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsubscribe = store.subscribe(() => {
|
||||||
|
const drives = getDrives();
|
||||||
|
const image = getImage();
|
||||||
|
this.setState({
|
||||||
|
drives,
|
||||||
|
image,
|
||||||
|
selectedList:
|
||||||
|
(this.props.updateSelectedList && this.props.updateSelectedList()) ||
|
||||||
|
[],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubscribe?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { cancel, done, ...props } = this.props;
|
||||||
|
const { selectedList, drives, image, missingDriversModal } = this.state;
|
||||||
|
|
||||||
|
const displayedDrives = this.getDisplayedDrives(drives);
|
||||||
|
const disabledDrives = this.getDisabledDrives(drives, image);
|
||||||
|
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
|
||||||
|
const numberOfDisplayedSystemDrives =
|
||||||
|
displayedDrives.filter(isSystemDrive).length;
|
||||||
|
const numberOfHiddenSystemDrives =
|
||||||
|
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||||
|
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
|
||||||
|
const showWarnings = this.props.showWarnings && hasSystemDrives;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
titleElement={
|
||||||
|
<Flex alignItems="baseline" mb={18}>
|
||||||
|
<Txt fontSize={24} align="left">
|
||||||
|
{this.props.titleLabel}
|
||||||
|
</Txt>
|
||||||
|
<Txt
|
||||||
|
fontSize={11}
|
||||||
|
ml={12}
|
||||||
|
color="#5b82a7"
|
||||||
|
style={{ fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
{drives.length} found
|
||||||
|
</Txt>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||||
|
cancel={() => cancel(this.originalList)}
|
||||||
|
done={() => done(selectedList)}
|
||||||
|
action={`Select (${selectedList.length})`}
|
||||||
|
primaryButtonProps={{
|
||||||
|
primary: !showWarnings,
|
||||||
|
warning: showWarnings,
|
||||||
|
disabled: !hasAvailableDrives(),
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{!hasAvailableDrives() ? (
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
{this.props.emptyListIcon}
|
||||||
|
<b>{this.props.emptyListLabel}</b>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<DrivesTable
|
||||||
|
refFn={(t) => {
|
||||||
|
if (t !== null) {
|
||||||
|
t.setRowSelection(selectedList);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
checkedRowsNumber={selectedList.length}
|
||||||
|
multipleSelection={this.props.multipleSelection}
|
||||||
|
columns={this.tableColumns}
|
||||||
|
data={displayedDrives}
|
||||||
|
disabledRows={disabledDrives}
|
||||||
|
getRowClass={(row: Drive) =>
|
||||||
|
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
|
||||||
|
}
|
||||||
|
rowKey="displayName"
|
||||||
|
onCheck={(rows: Drive[]) => {
|
||||||
|
let newSelection = rows.filter(isDrivelistDrive);
|
||||||
|
if (this.props.multipleSelection) {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
newSelection = [];
|
||||||
|
}
|
||||||
|
const deselecting = selectedList.filter(
|
||||||
|
(selected) =>
|
||||||
|
newSelection.filter(
|
||||||
|
(row) => row.device === selected.device,
|
||||||
|
).length === 0,
|
||||||
|
);
|
||||||
|
const selecting = newSelection.filter(
|
||||||
|
(row) =>
|
||||||
|
selectedList.filter(
|
||||||
|
(selected) => row.device === selected.device,
|
||||||
|
).length === 0,
|
||||||
|
);
|
||||||
|
deselecting.concat(selecting).forEach((row) => {
|
||||||
|
if (this.props.onSelect) {
|
||||||
|
this.props.onSelect(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
selectedList: newSelection,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.props.onSelect) {
|
||||||
|
this.props.onSelect(newSelection[newSelection.length - 1]);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
selectedList: newSelection.slice(newSelection.length - 1),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onRowClick={(row: Drive) => {
|
||||||
|
if (
|
||||||
|
!isDrivelistDrive(row) ||
|
||||||
|
this.driveShouldBeDisabled(row, image)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.props.onSelect) {
|
||||||
|
this.props.onSelect(row);
|
||||||
|
}
|
||||||
|
const index = selectedList.findIndex(
|
||||||
|
(d) => d.device === row.device,
|
||||||
|
);
|
||||||
|
const newList = this.props.multipleSelection
|
||||||
|
? [...selectedList]
|
||||||
|
: [];
|
||||||
|
if (index === -1) {
|
||||||
|
newList.push(row);
|
||||||
|
} else {
|
||||||
|
// Deselect if selected
|
||||||
|
newList.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
selectedList: newList,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{numberOfHiddenSystemDrives > 0 && (
|
||||||
|
<Link
|
||||||
|
mt={15}
|
||||||
|
mb={15}
|
||||||
|
fontSize="14px"
|
||||||
|
onClick={() => this.setState({ showSystemDrives: true })}
|
||||||
|
>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||||
|
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
|
||||||
|
</Flex>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{this.props.showWarnings && hasSystemDrives ? (
|
||||||
|
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||||
|
Selecting your system drive is dangerous and will erase your drive!
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{missingDriversModal.drive !== undefined && (
|
||||||
|
<Modal
|
||||||
|
width={400}
|
||||||
|
title={missingDriversModal.drive.linkTitle}
|
||||||
|
cancel={() => this.setState({ missingDriversModal: {} })}
|
||||||
|
done={() => {
|
||||||
|
try {
|
||||||
|
if (missingDriversModal.drive !== undefined) {
|
||||||
|
openExternal(missingDriversModal.drive.link);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logException(error);
|
||||||
|
} finally {
|
||||||
|
this.setState({ missingDriversModal: {} });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
action="Yes, continue"
|
||||||
|
cancelButtonProps={{
|
||||||
|
children: 'Cancel',
|
||||||
|
}}
|
||||||
|
children={
|
||||||
|
missingDriversModal.drive.linkMessage ||
|
||||||
|
`Etcher will open ${missingDriversModal.drive.link} in your browser`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = function (ModalService, $q) {
|
|
||||||
let modal = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Open the drive selector widget
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @fulfil {(Object|Undefined)} - selected drive
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorService.open().then((drive) => {
|
|
||||||
* console.log(drive);
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.open = () => {
|
|
||||||
modal = ModalService.open({
|
|
||||||
name: 'drive-selector',
|
|
||||||
template: require('../templates/drive-selector-modal.tpl.html'),
|
|
||||||
controller: 'DriveSelectorController as modal',
|
|
||||||
size: 'drive-selector-modal'
|
|
||||||
})
|
|
||||||
|
|
||||||
return modal.result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Close the drive selector widget
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @fulfil {Undefined}
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorService.close();
|
|
||||||
*/
|
|
||||||
this.close = () => {
|
|
||||||
if (modal) {
|
|
||||||
return modal.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve `undefined` if the modal
|
|
||||||
// was already closed for consistency
|
|
||||||
return $q.resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.modal-drive-selector-modal .modal-content {
|
|
||||||
width: 315px;
|
|
||||||
height: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-drive-selector-modal .modal-body {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-drive-selector-modal .list-group-item[disabled] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-drive-selector-modal {
|
|
||||||
|
|
||||||
.list-group-item-footer:has(span) {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-heading,
|
|
||||||
.list-group-item-text {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-left: 0;
|
|
||||||
border-right: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
border-color: darken($palette-theme-light-background, 7%);
|
|
||||||
padding: 12px 0;
|
|
||||||
|
|
||||||
.list-group-item-section-expanded {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-section + .list-group-item-section {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .tick {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled] .list-group-item-heading {
|
|
||||||
color: $palette-theme-light-soft-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress {
|
|
||||||
appearance: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 2.5px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress::-webkit-progress-bar {
|
|
||||||
background-color: $palette-theme-default-background;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress::-webkit-progress-value {
|
|
||||||
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
|
||||||
background-color: $palette-theme-primary-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-heading {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-text {
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 11px;
|
|
||||||
color: $palette-theme-light-soft-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.word-keep {
|
|
||||||
word-break: keep-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<div class="modal-header">
|
|
||||||
<h4 class="modal-title">Select a Drive</h4>
|
|
||||||
<button tabindex="14" class="close" ng-click="modal.closeModal()">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<ul class="list-group">
|
|
||||||
<li class="list-group-item" ng-repeat="drive in modal.getDrives() track by drive.device"
|
|
||||||
ng-disabled="!modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
|
||||||
ng-dblclick="modal.selectDriveAndClose(drive)"
|
|
||||||
ng-click="modal.toggleDrive(drive)">
|
|
||||||
<img class="list-group-item-section" alt="Drive device type logo"
|
|
||||||
ng-if="drive.icon"
|
|
||||||
ng-src="../assets/{{drive.icon}}.svg"
|
|
||||||
width="25"
|
|
||||||
height="30">
|
|
||||||
<div
|
|
||||||
class="list-group-item-section list-group-item-section-expanded"
|
|
||||||
tabindex="{{ 15 + $index }}"
|
|
||||||
ng-keypress="modal.keyboardToggleDrive(drive, $event)">
|
|
||||||
|
|
||||||
<h4 class="list-group-item-heading">{{ drive.description }}
|
|
||||||
<span class="word-keep"
|
|
||||||
ng-show="drive.size"> - {{ drive.size | closestUnit }}</span>
|
|
||||||
</h4>
|
|
||||||
<p class="list-group-item-text">{{ drive.displayName }}</p>
|
|
||||||
|
|
||||||
<footer class="list-group-item-footer">
|
|
||||||
|
|
||||||
<span class="label" ng-repeat="status in modal.getDriveStatuses(drive)"
|
|
||||||
ng-class="{
|
|
||||||
'label-warning': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.WARNING,
|
|
||||||
'label-danger': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.ERROR
|
|
||||||
}">{{ status.message }}</span>
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
<progress ng-if="drive.progress" value="{{ drive.progress }}" max="100"></progress>
|
|
||||||
</div>
|
|
||||||
<span class="list-group-item-section tick tick--success"
|
|
||||||
ng-show="modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
|
||||||
ng-disabled="!modal.state.isDriveSelected(drive.device)"></span>
|
|
||||||
</li>
|
|
||||||
<li class="list-group-item"
|
|
||||||
ng-show="!modal.drives.hasAvailableDrives()">
|
|
||||||
<div>
|
|
||||||
<b>Connect a drive!</b>
|
|
||||||
<div>No removable drive detected.</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="button button-primary button-block"
|
|
||||||
tabindex="{{ 15 + modal.getDrives().length }}"
|
|
||||||
ng-class="{
|
|
||||||
'button-warning': modal.constraints.hasListDriveImageCompatibilityStatus(modal.state.getSelectedDrives(), modal.state.getImage())
|
|
||||||
}"
|
|
||||||
ng-click="modal.closeModal()"
|
|
||||||
ng-disabled="!modal.state.hasDrive()">Continue</button>
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Badge, Flex, Txt, ModalProps } from 'rendition';
|
||||||
|
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||||
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
|
||||||
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
|
import { DriveWithWarnings } from '../../pages/main/Flash';
|
||||||
|
|
||||||
|
const DriveStatusWarningModal = ({
|
||||||
|
done,
|
||||||
|
cancel,
|
||||||
|
isSystem,
|
||||||
|
drivesWithWarnings,
|
||||||
|
}: ModalProps & {
|
||||||
|
isSystem: boolean;
|
||||||
|
drivesWithWarnings: DriveWithWarnings[];
|
||||||
|
}) => {
|
||||||
|
let warningSubtitle = 'You are about to erase an unusually large drive';
|
||||||
|
let warningCta = 'Are you sure the selected drive is not a storage drive?';
|
||||||
|
|
||||||
|
if (isSystem) {
|
||||||
|
warningSubtitle = "You are about to erase your computer's drives";
|
||||||
|
warningCta = 'Are you sure you want to flash your system drive?';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
footerShadow={false}
|
||||||
|
reverseFooterButtons={true}
|
||||||
|
done={done}
|
||||||
|
cancel={cancel}
|
||||||
|
cancelButtonProps={{
|
||||||
|
primary: false,
|
||||||
|
warning: true,
|
||||||
|
children: 'Change target',
|
||||||
|
}}
|
||||||
|
action={"Yes, I'm sure"}
|
||||||
|
primaryButtonProps={{
|
||||||
|
primary: false,
|
||||||
|
outline: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<ExclamationTriangleSvg height="2em" fill="#fca321" />
|
||||||
|
<Txt fontSize="24px" color="#fca321">
|
||||||
|
WARNING!
|
||||||
|
</Txt>
|
||||||
|
</Flex>
|
||||||
|
<Txt fontSize="24px">{warningSubtitle}</Txt>
|
||||||
|
<ScrollableFlex
|
||||||
|
flexDirection="column"
|
||||||
|
backgroundColor="#fff5e6"
|
||||||
|
m="2em 0"
|
||||||
|
p="1em 2em"
|
||||||
|
width="420px"
|
||||||
|
maxHeight="100px"
|
||||||
|
>
|
||||||
|
{drivesWithWarnings.map((drive, i, array) => (
|
||||||
|
<>
|
||||||
|
<Flex justifyContent="space-between" alignItems="baseline">
|
||||||
|
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
|
||||||
|
{drive.size && prettyBytes(drive.size) + ' '}
|
||||||
|
<Badge shade={5}>{drive.statuses[0].message}</Badge>
|
||||||
|
</Flex>
|
||||||
|
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</ScrollableFlex>
|
||||||
|
<Txt style={{ fontWeight: 600 }}>{warningCta}</Txt>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DriveStatusWarningModal;
|
||||||
124
lib/gui/app/components/finish/finish.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* 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 * 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 { FlashAnother } from '../flash-another/flash-another';
|
||||||
|
import { FlashResults, FlashError } from '../flash-results/flash-results';
|
||||||
|
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||||
|
|
||||||
|
function restart(goToMain: () => void) {
|
||||||
|
selectionState.deselectAllDrives();
|
||||||
|
analytics.logEvent('Restart');
|
||||||
|
|
||||||
|
// Reset the flashing workflow uuid
|
||||||
|
store.dispatch({
|
||||||
|
type: Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||||
|
data: uuidV4(),
|
||||||
|
});
|
||||||
|
|
||||||
|
goToMain();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSuccessBannerURL() {
|
||||||
|
return (
|
||||||
|
(await settings.get('successBannerURL')) ??
|
||||||
|
'https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||||
|
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 (
|
||||||
|
<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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FinishPage;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2019 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -14,15 +14,18 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.modal-warning-modal .modal-content {
|
import * as React from 'react';
|
||||||
width: 350px;
|
|
||||||
|
import { BaseButton } from '../../styled-components';
|
||||||
|
|
||||||
|
export interface FlashAnotherProps {
|
||||||
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-warning-modal .modal-title .glyphicon {
|
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||||
color: $palette-theme-danger-background;
|
return (
|
||||||
}
|
<BaseButton primary onClick={props.onClick}>
|
||||||
|
Flash another
|
||||||
.modal-warning-modal .modal-body {
|
</BaseButton>
|
||||||
max-height: 200px;
|
);
|
||||||
overflow-y: auto;
|
};
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.FlashErrorModal
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Components.FlashErrorModal'
|
|
||||||
const FlashErrorModal = angular.module(MODULE_NAME, [
|
|
||||||
require('../warning-modal/warning-modal')
|
|
||||||
])
|
|
||||||
|
|
||||||
FlashErrorModal.service('FlashErrorModalService', require('./services/flash-error-modal'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const flashState = require('../../../../../shared/models/flash-state')
|
|
||||||
const selectionState = require('../../../../../shared/models/selection-state')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
|
|
||||||
module.exports = function (WarningModalService) {
|
|
||||||
/**
|
|
||||||
* @summary Open the flash error modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {String} message - flash error message
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* FlashErrorModalService.show('The drive is not large enough!');
|
|
||||||
*/
|
|
||||||
this.show = (message) => {
|
|
||||||
return WarningModalService.display({
|
|
||||||
confirmationLabel: 'Retry',
|
|
||||||
description: message
|
|
||||||
}).then((confirmed) => {
|
|
||||||
flashState.resetState()
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
analytics.logEvent('Restart after failure')
|
|
||||||
} else {
|
|
||||||
selectionState.clear()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
243
lib/gui/app/components/flash-results/flash-results.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
* 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 CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||||
|
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
|
||||||
|
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
|
||||||
|
import outdent from 'outdent';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { progress } from '../../../../shared/messages';
|
||||||
|
import { bytesToMegabytes } from '../../../../shared/units';
|
||||||
|
|
||||||
|
import FlashSvg from '../../../assets/flash.svg';
|
||||||
|
import { getDrives } from '../../models/available-drives';
|
||||||
|
import { resetState } from '../../models/flash-state';
|
||||||
|
import * as selection from '../../models/selection-state';
|
||||||
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
import { Modal, Table } from '../../styled-components';
|
||||||
|
|
||||||
|
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
|
||||||
|
&&& [data-display='table-head'],
|
||||||
|
&&& [data-display='table-body'] {
|
||||||
|
> [data-display='table-row'] {
|
||||||
|
> [data-display='table-cell'] {
|
||||||
|
&:first-child {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const DoneIcon = (props: {
|
||||||
|
skipped: boolean;
|
||||||
|
color: string;
|
||||||
|
allFailed: boolean;
|
||||||
|
}) => {
|
||||||
|
const svgProps = {
|
||||||
|
width: '28px',
|
||||||
|
fill: props.color,
|
||||||
|
style: {
|
||||||
|
marginTop: '-25px',
|
||||||
|
marginLeft: '13px',
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return props.allFailed && !props.skipped ? (
|
||||||
|
<TimesCircleSvg {...svgProps} />
|
||||||
|
) : (
|
||||||
|
<CheckCircleSvg {...svgProps} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FlashError extends Error {
|
||||||
|
description: string;
|
||||||
|
device: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formattedErrors(errors: FlashError[]) {
|
||||||
|
return errors
|
||||||
|
.map((error) => `${error.device}: ${error.message || error.code}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: Array<TableColumn<FlashError>> = [
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
label: 'Target',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'device',
|
||||||
|
label: 'Location',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'message',
|
||||||
|
label: 'Error',
|
||||||
|
render: (message: string, { code }: FlashError) => {
|
||||||
|
return message ?? code;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getEffectiveSpeed(results: {
|
||||||
|
sourceMetadata: {
|
||||||
|
size: number;
|
||||||
|
blockmappedSize?: number;
|
||||||
|
};
|
||||||
|
averageFlashingSpeed: number;
|
||||||
|
}) {
|
||||||
|
const flashedSize =
|
||||||
|
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
|
||||||
|
const timeSpent = flashedSize / results.averageFlashingSpeed;
|
||||||
|
return results.sourceMetadata.size / timeSpent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlashResults({
|
||||||
|
goToMain,
|
||||||
|
image = '',
|
||||||
|
errors,
|
||||||
|
results,
|
||||||
|
skip,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
goToMain: () => void;
|
||||||
|
image?: string;
|
||||||
|
errors: FlashError[];
|
||||||
|
skip: boolean;
|
||||||
|
results: {
|
||||||
|
sourceMetadata: {
|
||||||
|
size: number;
|
||||||
|
blockmappedSize?: number;
|
||||||
|
};
|
||||||
|
averageFlashingSpeed: number;
|
||||||
|
devices: { failed: number; successful: number };
|
||||||
|
};
|
||||||
|
} & FlexProps) {
|
||||||
|
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||||
|
const allFailed = !skip && results.devices.successful === 0;
|
||||||
|
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
|
||||||
|
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Flex flexDirection="column" {...props}>
|
||||||
|
<Flex alignItems="center" flexDirection="column">
|
||||||
|
<Flex
|
||||||
|
alignItems="center"
|
||||||
|
mt="50px"
|
||||||
|
mb="32px"
|
||||||
|
color="#7e8085"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<FlashSvg width="40px" height="40px" className="disabled" />
|
||||||
|
<DoneIcon
|
||||||
|
skipped={skip}
|
||||||
|
allFailed={allFailed}
|
||||||
|
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
|
||||||
|
/>
|
||||||
|
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||||
|
</Flex>
|
||||||
|
<Txt fontSize={24} color="#fff" mb="17px">
|
||||||
|
Flash {allFailed ? 'Failed' : 'Complete'}!
|
||||||
|
</Txt>
|
||||||
|
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
|
||||||
|
</Flex>
|
||||||
|
<Flex flexDirection="column" color="#7e8085">
|
||||||
|
{results.devices.successful !== 0 ? (
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<CircleSvg width="14px" fill="#1ac135" />
|
||||||
|
<Txt ml="10px" color="#fff">
|
||||||
|
{results.devices.successful}
|
||||||
|
</Txt>
|
||||||
|
<Txt ml="10px">
|
||||||
|
{progress.successful(results.devices.successful)}
|
||||||
|
</Txt>
|
||||||
|
</Flex>
|
||||||
|
) : null}
|
||||||
|
{errors.length !== 0 ? (
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<CircleSvg width="14px" fill="#ff4444" />
|
||||||
|
<Txt ml="10px" color="#fff">
|
||||||
|
{errors.length}
|
||||||
|
</Txt>
|
||||||
|
<Txt ml="10px" tooltip={formattedErrors(errors)}>
|
||||||
|
{progress.failed(errors.length)}
|
||||||
|
</Txt>
|
||||||
|
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||||
|
more info
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
) : null}
|
||||||
|
{!allFailed && (
|
||||||
|
<Txt
|
||||||
|
fontSize="10px"
|
||||||
|
style={{
|
||||||
|
fontWeight: 500,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
tooltip={outdent({ newline: ' ' })`
|
||||||
|
The speed is calculated by dividing the image size by the flashing time.
|
||||||
|
Disk images with ext partitions flash faster as we are able to skip unused parts.
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Effective speed: {effectiveSpeed} MB/s
|
||||||
|
</Txt>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{showErrorsInfo && (
|
||||||
|
<Modal
|
||||||
|
titleElement={
|
||||||
|
<Flex alignItems="baseline" mb={18}>
|
||||||
|
<Txt fontSize={24} align="left">
|
||||||
|
Failed targets
|
||||||
|
</Txt>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
action="Retry failed targets"
|
||||||
|
cancel={() => setShowErrorsInfo(false)}
|
||||||
|
done={() => {
|
||||||
|
setShowErrorsInfo(false);
|
||||||
|
resetState();
|
||||||
|
getDrives()
|
||||||
|
.map((drive) => {
|
||||||
|
selection.deselectDrive(drive.device);
|
||||||
|
return drive.device;
|
||||||
|
})
|
||||||
|
.filter((driveDevice) =>
|
||||||
|
errors.some((error) => error.device === driveDevice),
|
||||||
|
)
|
||||||
|
.forEach((driveDevice) => selection.selectDrive(driveDevice));
|
||||||
|
goToMain();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorsTable columns={columns} data={errors} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
|
|
||||||
module.exports = function ($uibModal, $q) {
|
|
||||||
/**
|
|
||||||
* @summary Open a modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} options - options
|
|
||||||
* @param {String} options.template - template contents
|
|
||||||
* @param {String} options.controller - controller
|
|
||||||
* @param {String} [options.size='sm'] - modal size
|
|
||||||
* @param {Object} options.resolve - modal resolves
|
|
||||||
* @returns {Object} modal
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ModalService.open({
|
|
||||||
* name: 'my modal',
|
|
||||||
* template: require('./path/to/modal.tpl.html'),
|
|
||||||
* controller: 'DriveSelectorController as modal',
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.open = (options = {}) => {
|
|
||||||
_.defaults(options, {
|
|
||||||
size: 'sm'
|
|
||||||
})
|
|
||||||
|
|
||||||
analytics.logEvent('Open modal', {
|
|
||||||
name: options.name
|
|
||||||
})
|
|
||||||
|
|
||||||
const modal = $uibModal.open({
|
|
||||||
animation: true,
|
|
||||||
template: options.template,
|
|
||||||
controller: options.controller,
|
|
||||||
size: options.size,
|
|
||||||
resolve: options.resolve
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
close: modal.close,
|
|
||||||
result: $q((resolve, reject) => {
|
|
||||||
modal.result.then((value) => {
|
|
||||||
analytics.logEvent('Modal accepted', {
|
|
||||||
name: options.name,
|
|
||||||
value
|
|
||||||
})
|
|
||||||
|
|
||||||
resolve(value)
|
|
||||||
}).catch((error) => {
|
|
||||||
// Bootstrap doesn't 'resolve' these but cancels the dialog
|
|
||||||
if (error === 'escape key press' || error === 'backdrop click') {
|
|
||||||
analytics.logEvent('Modal rejected', {
|
|
||||||
name: options.name,
|
|
||||||
method: error
|
|
||||||
})
|
|
||||||
|
|
||||||
return resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
analytics.logEvent('Modal rejected', {
|
|
||||||
name: options.name,
|
|
||||||
value: error
|
|
||||||
})
|
|
||||||
|
|
||||||
return reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background-color: $palette-theme-light-background;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0 auto;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
font-size: 12px;
|
|
||||||
color: $palette-theme-light-soft-foreground;
|
|
||||||
padding: 11px 20px;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
font-size: inherit;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
flex-grow: 1;
|
|
||||||
color: $palette-theme-light-foreground;
|
|
||||||
padding: 20px;
|
|
||||||
max-height: 250px;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $palette-theme-primary-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
> p {
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
|
|
||||||
> p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-menu {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI Bootstrap adds the `.modal-open` class to the <body>
|
|
||||||
// element and sets its right padding to the width of the
|
|
||||||
// window, causing the window content to overflow and get
|
|
||||||
// pushed to the bottom.
|
|
||||||
// The `!important` flag is needed since UI Bootstrap inlines
|
|
||||||
// the styles programmatically to the element.
|
|
||||||
.modal-open {
|
|
||||||
padding-right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable modal opacity
|
|
||||||
.modal-backdrop.in {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
flex-grow: 0;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
|
|
||||||
// Center the modal using Flexbox so we can
|
|
||||||
// freely use any height.
|
|
||||||
display: flex !important;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.button[disabled] {
|
|
||||||
background-color: $palette-theme-light-disabled-background;
|
|
||||||
color: $palette-theme-light-disabled-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-dialog {
|
|
||||||
margin: 0;
|
|
||||||
position: initial;
|
|
||||||
max-width: 50%;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary ProgressButton directive
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This directive provides a button containing a progress bar inside.
|
|
||||||
* The button is styled by default as a primary button.
|
|
||||||
*
|
|
||||||
* @returns {Object} directive
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <progress-button percentage="{{ 40 }}" striped>My Progress Button</progress-button>
|
|
||||||
*/
|
|
||||||
module.exports = () => {
|
|
||||||
return {
|
|
||||||
template: require('../templates/progress-button.tpl.html'),
|
|
||||||
restrict: 'E',
|
|
||||||
replace: true,
|
|
||||||
transclude: true,
|
|
||||||
scope: {
|
|
||||||
percentage: '=',
|
|
||||||
striped: '@'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.ProgressButton
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Components.ProgressButton'
|
|
||||||
const ProgressButton = angular.module(MODULE_NAME, [])
|
|
||||||
ProgressButton.directive('progressButton', require('./directives/progress-button'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
||||||
133
lib/gui/app/components/progress-button/progress-button.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* 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, Button, ProgressBar, Txt } from 'rendition';
|
||||||
|
import { default as styled } from 'styled-components';
|
||||||
|
|
||||||
|
import { fromFlashState } from '../../modules/progress-status';
|
||||||
|
import { StepButton } from '../../styled-components';
|
||||||
|
|
||||||
|
const FlashProgressBar = styled(ProgressBar)`
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
height: 12px;
|
||||||
|
color: white !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
transition-duration: 0s;
|
||||||
|
> div {
|
||||||
|
transition-duration: 0s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 48px;
|
||||||
|
|
||||||
|
background: #2f3033;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ProgressButtonProps {
|
||||||
|
type: 'decompressing' | 'flashing' | 'verifying';
|
||||||
|
active: boolean;
|
||||||
|
percentage: number;
|
||||||
|
position: number;
|
||||||
|
disabled: boolean;
|
||||||
|
cancel: (type: string) => void;
|
||||||
|
callback: () => void;
|
||||||
|
warning?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
decompressing: '#00aeef',
|
||||||
|
flashing: '#da60ff',
|
||||||
|
verifying: '#1ac135',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||||
|
const status = type === 'verifying' ? 'Skip' : 'Cancel';
|
||||||
|
return (
|
||||||
|
<Button plain onClick={() => onClick(status)} {...props}>
|
||||||
|
{status}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})`
|
||||||
|
font-weight: 600;
|
||||||
|
&&& {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||||
|
public render() {
|
||||||
|
const percentage = this.props.percentage;
|
||||||
|
const warning = this.props.warning;
|
||||||
|
const { status, position } = fromFlashState({
|
||||||
|
type: this.props.type,
|
||||||
|
percentage,
|
||||||
|
position: this.props.position,
|
||||||
|
});
|
||||||
|
const type = this.props.type || 'default';
|
||||||
|
if (this.props.active) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
alignItems="baseline"
|
||||||
|
justifyContent="space-between"
|
||||||
|
width="100%"
|
||||||
|
style={{
|
||||||
|
marginTop: 42,
|
||||||
|
marginBottom: '6px',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex>
|
||||||
|
<Txt color="#fff">{status} </Txt>
|
||||||
|
<Txt color={colors[type]}>{position}</Txt>
|
||||||
|
</Flex>
|
||||||
|
{type && (
|
||||||
|
<CancelButton
|
||||||
|
type={type}
|
||||||
|
onClick={this.props.cancel}
|
||||||
|
color="#00aeef"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<FlashProgressBar background={colors[type]} value={percentage} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<StepButton
|
||||||
|
primary={!warning}
|
||||||
|
warning={warning}
|
||||||
|
onClick={this.props.callback}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
style={{
|
||||||
|
marginTop: 30,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Flash!
|
||||||
|
</StepButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A button with a progress bar inside.
|
|
||||||
*
|
|
||||||
* From http://tympanus.net/Development/ProgressButtonStyles/
|
|
||||||
*
|
|
||||||
* The state of the progress bar is controller by the width, in percentage,
|
|
||||||
* of `.progress-button__bar`.
|
|
||||||
*
|
|
||||||
* If there is an action in place, the `active` attribute must be set to `true`.
|
|
||||||
* This is useful to determine if the progress bar is paused from the point of view
|
|
||||||
* of the styling.
|
|
||||||
*
|
|
||||||
* You can optionally pass the `.progress-button--striped` modified to get a striped
|
|
||||||
* progress bar.
|
|
||||||
*
|
|
||||||
* The stripe implementation idea was taken from:
|
|
||||||
*
|
|
||||||
* https://css-tricks.com/css3-progress-bars/
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
*
|
|
||||||
* <button class="progress-button" active="true">
|
|
||||||
* <span class="progress-button__content">Button text</span>
|
|
||||||
* <span class="progress-button__bar" style="width: 50%;"></span>
|
|
||||||
* </button>
|
|
||||||
*/
|
|
||||||
|
|
||||||
$progress-button-stripes-width: 20px;
|
|
||||||
$progress-button-stripes-animation-duration: 1s;
|
|
||||||
|
|
||||||
.progress-button {
|
|
||||||
@extend .button;
|
|
||||||
@extend .button-primary;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&[active="true"] {
|
|
||||||
background-color: $palette-theme-warning-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-button__bar {
|
|
||||||
background-color: lighten($palette-theme-warning-background, 5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.progress-button--striped {
|
|
||||||
$progress-button-stripes-background-color: desaturate($palette-theme-primary-background, 5%);
|
|
||||||
$progress-button-stripes-foreground-color: desaturate(darken($palette-theme-primary-background, 18%), 20%);
|
|
||||||
|
|
||||||
// Notice that we add `0.01` to certain gradient stop positions.
|
|
||||||
// That workarounds a Chrome rendering issue where diagonal
|
|
||||||
// lines look spiky.
|
|
||||||
// See https://github.com/resin-io/etcher/issues/472
|
|
||||||
background-image: -webkit-gradient(linear, 0 0, 100% 100%,
|
|
||||||
color-stop(0.25, $progress-button-stripes-foreground-color),
|
|
||||||
color-stop(0.25 + 0.01, $progress-button-stripes-background-color),
|
|
||||||
color-stop(0.50, $progress-button-stripes-background-color),
|
|
||||||
color-stop(0.50 + 0.01, $progress-button-stripes-foreground-color),
|
|
||||||
color-stop(0.75, $progress-button-stripes-foreground-color),
|
|
||||||
color-stop(0.75 + 0.01, $progress-button-stripes-background-color),
|
|
||||||
to($progress-button-stripes-background-color));
|
|
||||||
|
|
||||||
.progress-button__bar {
|
|
||||||
background-color: lighten($palette-theme-primary-background, 5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent the button from being clickable
|
|
||||||
// when it has an active progress bar.
|
|
||||||
.progress-button[active="true"] {
|
|
||||||
@extend .button-no-hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-button__content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-button__bar {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
width: 0;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
// Subtle progress bar animation
|
|
||||||
transition: width 0.3s;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-button--striped {
|
|
||||||
background-size: $progress-button-stripes-width $progress-button-stripes-width;
|
|
||||||
animation: progress-button-stripes $progress-button-stripes-animation-duration linear infinite;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes progress-button-stripes {
|
|
||||||
|
|
||||||
0% {
|
|
||||||
background-position: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: $progress-button-stripes-width $progress-button-stripes-width;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<button class="progress-button"
|
|
||||||
ng-class="{
|
|
||||||
'progress-button--striped': striped && striped != 'false'
|
|
||||||
}">
|
|
||||||
<span class="progress-button__content" ng-transclude></span>
|
|
||||||
<span class="progress-button__bar" ng-style="{ width: (percentage > 100 ? 100 : percentage) + '%' }"></span>
|
|
||||||
</button>
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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 DriveSvg from '../../../assets/drive.svg';
|
||||||
|
import ImageSvg from '../../../assets/image.svg';
|
||||||
|
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||||
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
|
||||||
|
interface ReducedFlashingInfosProps {
|
||||||
|
imageLogo?: string;
|
||||||
|
imageName?: string;
|
||||||
|
imageSize: string;
|
||||||
|
driveTitle: string;
|
||||||
|
driveLabel: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReducedFlashingInfos extends React.Component<ReducedFlashingInfosProps> {
|
||||||
|
constructor(props: ReducedFlashingInfosProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { imageName = '' } = this.props;
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
style={this.props.style ? this.props.style : undefined}
|
||||||
|
>
|
||||||
|
<Flex mb={16}>
|
||||||
|
<SVGIcon
|
||||||
|
disabled
|
||||||
|
width="21px"
|
||||||
|
height="21px"
|
||||||
|
contents={this.props.imageLogo}
|
||||||
|
fallback={ImageSvg}
|
||||||
|
style={{ marginRight: '9px' }}
|
||||||
|
/>
|
||||||
|
<Txt
|
||||||
|
style={{ marginRight: '9px' }}
|
||||||
|
tooltip={{ text: imageName, placement: 'right' }}
|
||||||
|
>
|
||||||
|
{middleEllipsis(imageName, 16)}
|
||||||
|
</Txt>
|
||||||
|
<Txt color="#7e8085">{this.props.imageSize}</Txt>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/* eslint-disable jsdoc/require-example */
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const electron = require('electron')
|
|
||||||
const angular = require('angular')
|
|
||||||
const react = require('react')
|
|
||||||
const propTypes = require('prop-types')
|
|
||||||
const { react2angular } = require('react2angular')
|
|
||||||
const analytics = require('../modules/analytics')
|
|
||||||
const packageJSON = require('../../../../package.json')
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.SafeWebview'
|
|
||||||
const angularSafeWebview = angular.module(MODULE_NAME, [])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Electron session identifier
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
const ELECTRON_SESSION = 'persist:success-banner'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Etcher version search-parameter key
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
const ETCHER_VERSION_PARAM = 'etcher-version'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary API version search-parameter key
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
const API_VERSION_PARAM = 'api-version'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Webview API version
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
* @type {String}
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Changing this number represents a departure from an older API and as such
|
|
||||||
* should only be changed when truly necessary as it introduces breaking changes.
|
|
||||||
* This version number is exposed to the banner such that it can determine what
|
|
||||||
* features are safe to utilize.
|
|
||||||
*/
|
|
||||||
const API_VERSION = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Webviews that hide/show depending on the HTTP status returned
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <safe-webview src="https://etcher.io/"></safe-webview>
|
|
||||||
*/
|
|
||||||
class SafeWebview extends react.PureComponent {
|
|
||||||
/**
|
|
||||||
* @param {Object} props - React element properties
|
|
||||||
*/
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
shouldShow: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new window.URL(props.src)
|
|
||||||
|
|
||||||
// We set the version GET parameters here.
|
|
||||||
url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version)
|
|
||||||
url.searchParams.set(API_VERSION_PARAM, API_VERSION)
|
|
||||||
|
|
||||||
this.entryHref = url.href
|
|
||||||
|
|
||||||
// Events steal 'this'
|
|
||||||
this.didFailLoad = _.bind(this.didFailLoad, this)
|
|
||||||
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this)
|
|
||||||
|
|
||||||
this.eventTuples = [
|
|
||||||
[ 'did-fail-load', this.didFailLoad ],
|
|
||||||
[ 'did-get-response-details', this.didGetResponseDetails ],
|
|
||||||
[ 'new-window', this.constructor.newWindow ],
|
|
||||||
[ 'console-message', this.constructor.consoleMessage ]
|
|
||||||
]
|
|
||||||
|
|
||||||
// Make a persistent electron session for the webview
|
|
||||||
electron.remote.session.fromPartition(ELECTRON_SESSION, {
|
|
||||||
|
|
||||||
// Disable the cache for the session such that new content shows up when refreshing
|
|
||||||
cache: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {react.Element}
|
|
||||||
*/
|
|
||||||
render () {
|
|
||||||
return react.createElement('webview', {
|
|
||||||
ref: 'webview',
|
|
||||||
style: {
|
|
||||||
flex: this.state.shouldShow ? null : '0 1',
|
|
||||||
width: this.state.shouldShow ? null : '0',
|
|
||||||
height: this.state.shouldShow ? null : '0'
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Add the Webview events
|
|
||||||
*/
|
|
||||||
componentDidMount () {
|
|
||||||
// Events React is unaware of have to be handled manually
|
|
||||||
_.map(this.eventTuples, (tuple) => {
|
|
||||||
this.refs.webview.addEventListener(...tuple)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Use the 'success-banner' session
|
|
||||||
this.refs.webview.partition = ELECTRON_SESSION
|
|
||||||
|
|
||||||
// It's important that this comes after the partition setting, otherwise it will
|
|
||||||
// use another session and we can't change it without destroying the element again
|
|
||||||
this.refs.webview.src = this.entryHref
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Remove the Webview events
|
|
||||||
*/
|
|
||||||
componentWillUnmount () {
|
|
||||||
// Events that React is unaware of have to be handled manually
|
|
||||||
_.map(this.eventTuples, (tuple) => {
|
|
||||||
this.refs.webview.removeEventListener(...tuple)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Refresh the webview if we are navigating away from the success page
|
|
||||||
* @param {Object} nextProps - upcoming properties
|
|
||||||
*/
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
if (nextProps.refreshNow && !this.props.refreshNow) {
|
|
||||||
// Reload the page if it hasn't changed, otherwise reset the source URL,
|
|
||||||
// because reload interferes with 'src' setting, resetting the 'src' attribute
|
|
||||||
// to what it was was just prior.
|
|
||||||
if (this.refs.webview.src === this.entryHref) {
|
|
||||||
this.refs.webview.reload()
|
|
||||||
} else {
|
|
||||||
this.refs.webview.src = this.entryHref
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
shouldShow: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Set the element state to hidden
|
|
||||||
*/
|
|
||||||
didFailLoad () {
|
|
||||||
this.setState({
|
|
||||||
shouldShow: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Set the element state depending on the HTTP response code
|
|
||||||
* @param {Event} event - Event object
|
|
||||||
*/
|
|
||||||
didGetResponseDetails (event) {
|
|
||||||
// This seems to pick up all requests related to the webview,
|
|
||||||
// only care about this event if it's a request for the main frame
|
|
||||||
if (event.resourceType === 'mainFrame') {
|
|
||||||
const HTTP_OK = 200
|
|
||||||
|
|
||||||
analytics.logEvent(event)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
shouldShow: event.httpResponseCode === HTTP_OK
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Open link in browser if it's opened as a 'foreground-tab'
|
|
||||||
* @param {Event} event - event object
|
|
||||||
*/
|
|
||||||
static newWindow (event) {
|
|
||||||
const url = new window.URL(event.url)
|
|
||||||
|
|
||||||
if (_.every([
|
|
||||||
url.protocol === 'http:' || url.protocol === 'https:',
|
|
||||||
event.disposition === 'foreground-tab'
|
|
||||||
])) {
|
|
||||||
electron.shell.openExternal(url.href)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Forward specially-formatted console messages from the webview
|
|
||||||
* @param {Event} event - event object
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* // In the webview
|
|
||||||
* console.log('Good night!')
|
|
||||||
*/
|
|
||||||
static consoleMessage (event) {
|
|
||||||
if (_.isNil(event.message)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = event.message
|
|
||||||
try {
|
|
||||||
message = JSON.parse(event.message)
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.command === 'error') {
|
|
||||||
analytics.logException(message.data)
|
|
||||||
} else {
|
|
||||||
analytics.logEvent(message.data || message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SafeWebview.propTypes = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary The website source URL
|
|
||||||
*/
|
|
||||||
src: propTypes.string.isRequired,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Refresh the webview
|
|
||||||
*/
|
|
||||||
refreshNow: propTypes.bool
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
angularSafeWebview.component('safeWebview', react2angular(SafeWebview))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
||||||
208
lib/gui/app/components/safe-webview/safe-webview.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as electron from 'electron';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import * as packageJSON from '../../../../../package.json';
|
||||||
|
import * as settings from '../../models/settings';
|
||||||
|
import * as analytics from '../../modules/analytics';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Electron session identifier
|
||||||
|
*/
|
||||||
|
const ELECTRON_SESSION = 'persist:success-banner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Etcher version search-parameter key
|
||||||
|
*/
|
||||||
|
const ETCHER_VERSION_PARAM = 'etcher-version';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary API version search-parameter key
|
||||||
|
*/
|
||||||
|
const API_VERSION_PARAM = 'api-version';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Opt-out analytics search-parameter key
|
||||||
|
*/
|
||||||
|
const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Webview API version
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Changing this number represents a departure from an older API and as such
|
||||||
|
* should only be changed when truly necessary as it introduces breaking changes.
|
||||||
|
* This version number is exposed to the banner such that it can determine what
|
||||||
|
* features are safe to utilize.
|
||||||
|
*
|
||||||
|
* See `git blame -L n` where n is the line below for the history of version changes.
|
||||||
|
*/
|
||||||
|
const API_VERSION = '2';
|
||||||
|
|
||||||
|
interface SafeWebviewProps {
|
||||||
|
// The website source URL
|
||||||
|
src: string;
|
||||||
|
// Webview lifecycle event
|
||||||
|
onWebviewShow?: (isWebviewShowing: boolean) => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SafeWebviewState {
|
||||||
|
shouldShow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Webviews that hide/show depending on the HTTP status returned
|
||||||
|
*/
|
||||||
|
export class SafeWebview extends React.PureComponent<
|
||||||
|
SafeWebviewProps,
|
||||||
|
SafeWebviewState
|
||||||
|
> {
|
||||||
|
private entryHref: string;
|
||||||
|
private session: electron.Session;
|
||||||
|
private webviewRef: React.RefObject<electron.WebviewTag>;
|
||||||
|
|
||||||
|
constructor(props: SafeWebviewProps) {
|
||||||
|
super(props);
|
||||||
|
this.webviewRef = React.createRef();
|
||||||
|
this.state = {
|
||||||
|
shouldShow: true,
|
||||||
|
};
|
||||||
|
const url = new window.URL(this.props.src);
|
||||||
|
// We set the version GET parameters here.
|
||||||
|
url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version);
|
||||||
|
url.searchParams.set(API_VERSION_PARAM, API_VERSION);
|
||||||
|
url.searchParams.set(
|
||||||
|
OPT_OUT_ANALYTICS_PARAM,
|
||||||
|
(!settings.getSync('errorReporting')).toString(),
|
||||||
|
);
|
||||||
|
this.entryHref = url.href;
|
||||||
|
// Events steal 'this'
|
||||||
|
this.didFailLoad = _.bind(this.didFailLoad, this);
|
||||||
|
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
|
||||||
|
// Make a persistent electron session for the webview
|
||||||
|
this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, {
|
||||||
|
// Disable the cache for the session such that new content shows up when refreshing
|
||||||
|
cache: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static logWebViewMessage(event: electron.ConsoleMessageEvent) {
|
||||||
|
console.log('Message from SafeWebview:', event.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
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={style}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the Webview events
|
||||||
|
public componentDidMount() {
|
||||||
|
// Events React is unaware of have to be handled manually
|
||||||
|
if (this.webviewRef.current !== null) {
|
||||||
|
this.webviewRef.current.addEventListener(
|
||||||
|
'did-fail-load',
|
||||||
|
this.didFailLoad,
|
||||||
|
);
|
||||||
|
this.webviewRef.current.addEventListener(
|
||||||
|
'new-window',
|
||||||
|
SafeWebview.newWindow,
|
||||||
|
);
|
||||||
|
this.webviewRef.current.addEventListener(
|
||||||
|
'console-message',
|
||||||
|
SafeWebview.logWebViewMessage,
|
||||||
|
);
|
||||||
|
this.session.webRequest.onCompleted(this.didGetResponseDetails);
|
||||||
|
// It's important that this comes after the partition setting, otherwise it will
|
||||||
|
// use another session and we can't change it without destroying the element again
|
||||||
|
this.webviewRef.current.src = this.entryHref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the Webview events
|
||||||
|
public componentWillUnmount() {
|
||||||
|
// Events that React is unaware of have to be handled manually
|
||||||
|
if (this.webviewRef.current !== null) {
|
||||||
|
this.webviewRef.current.removeEventListener(
|
||||||
|
'did-fail-load',
|
||||||
|
this.didFailLoad,
|
||||||
|
);
|
||||||
|
this.webviewRef.current.removeEventListener(
|
||||||
|
'new-window',
|
||||||
|
SafeWebview.newWindow,
|
||||||
|
);
|
||||||
|
this.webviewRef.current.removeEventListener(
|
||||||
|
'console-message',
|
||||||
|
SafeWebview.logWebViewMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.session.webRequest.onCompleted(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the element state to hidden
|
||||||
|
public didFailLoad() {
|
||||||
|
this.setState({
|
||||||
|
shouldShow: false,
|
||||||
|
});
|
||||||
|
if (this.props.onWebviewShow) {
|
||||||
|
this.props.onWebviewShow(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the element state depending on the HTTP response code
|
||||||
|
public didGetResponseDetails(event: electron.OnCompletedListenerDetails) {
|
||||||
|
// This seems to pick up all requests related to the webview,
|
||||||
|
// only care about this event if it's a request for the main frame
|
||||||
|
if (event.resourceType === 'mainFrame') {
|
||||||
|
const HTTP_OK = 200;
|
||||||
|
analytics.logEvent('SafeWebview loaded', { event });
|
||||||
|
this.setState({
|
||||||
|
shouldShow: event.statusCode === HTTP_OK,
|
||||||
|
});
|
||||||
|
if (this.props.onWebviewShow) {
|
||||||
|
this.props.onWebviewShow(event.statusCode === HTTP_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open link in browser if it's opened as a 'foreground-tab'
|
||||||
|
public static async newWindow(event: electron.NewWindowEvent) {
|
||||||
|
const url = new window.URL(event.url);
|
||||||
|
if (
|
||||||
|
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
||||||
|
event.disposition === 'foreground-tab' &&
|
||||||
|
// Don't open links if they're disabled by the env var
|
||||||
|
!(await settings.get('disableExternalLinks'))
|
||||||
|
) {
|
||||||
|
electron.shell.openExternal(url.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
lib/gui/app/components/settings/settings.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* 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 GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
|
||||||
|
|
||||||
|
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';
|
||||||
|
import { Modal } from '../../styled-components';
|
||||||
|
|
||||||
|
interface Setting {
|
||||||
|
name: string;
|
||||||
|
label: string | JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSettingsList(): Promise<Setting[]> {
|
||||||
|
const list: Setting[] = [
|
||||||
|
{
|
||||||
|
name: 'errorReporting',
|
||||||
|
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||||
|
list.push({
|
||||||
|
name: 'updatesEnabled',
|
||||||
|
label: 'Auto-updates enabled',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsModalProps {
|
||||||
|
toggleModal: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UUID = process.env.BALENA_DEVICE_UUID;
|
||||||
|
|
||||||
|
const InfoBox = (props: any) => (
|
||||||
|
<Box fontSize={14}>
|
||||||
|
<Txt>{props.label}</Txt>
|
||||||
|
<TextWithCopy code text={props.value} copy={props.value} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||||
|
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (settingsList.length === 0) {
|
||||||
|
setCurrentSettingsList(await getSettingsList());
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
const [currentSettings, setCurrentSettings] = React.useState<
|
||||||
|
_.Dictionary<boolean>
|
||||||
|
>({});
|
||||||
|
React.useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (_.isEmpty(currentSettings)) {
|
||||||
|
setCurrentSettings(await settings.getAll());
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleSetting = async (setting: string) => {
|
||||||
|
const value = currentSettings[setting];
|
||||||
|
analytics.logEvent('Toggle setting', { setting, value });
|
||||||
|
await settings.set(setting, !value);
|
||||||
|
setCurrentSettings({
|
||||||
|
...currentSettings,
|
||||||
|
[setting]: !value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
titleElement={
|
||||||
|
<Txt fontSize={24} mb={24}>
|
||||||
|
Settings
|
||||||
|
</Txt>
|
||||||
|
}
|
||||||
|
done={() => toggleModal(false)}
|
||||||
|
>
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
{settingsList.map((setting: Setting, i: number) => {
|
||||||
|
return (
|
||||||
|
<Flex key={setting.name} mb={14}>
|
||||||
|
<Checkbox
|
||||||
|
toggle
|
||||||
|
tabIndex={6 + i}
|
||||||
|
label={setting.label}
|
||||||
|
checked={currentSettings[setting.name]}
|
||||||
|
onChange={() => toggleSetting(setting.name)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{UUID !== undefined && (
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<Txt fontSize={24}>System Information</Txt>
|
||||||
|
<InfoBox label="UUID" value={UUID.substr(0, 7)} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<Flex
|
||||||
|
mt={18}
|
||||||
|
alignItems="center"
|
||||||
|
color="#00aeef"
|
||||||
|
style={{
|
||||||
|
width: 'fit-content',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
openExternal(
|
||||||
|
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<GithubSvg
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
/>
|
||||||
|
<Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
878
lib/gui/app/components/source-selector/source-selector.tsx
Normal file
@@ -0,0 +1,878 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
|
||||||
|
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
||||||
|
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||||
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||||
|
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||||
|
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
|
||||||
|
import { sourceDestination } from 'etcher-sdk';
|
||||||
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
ButtonProps,
|
||||||
|
Modal as SmallModal,
|
||||||
|
Txt,
|
||||||
|
Card as BaseCard,
|
||||||
|
Input,
|
||||||
|
Spinner,
|
||||||
|
Link,
|
||||||
|
} from 'rendition';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import * as errors from '../../../../shared/errors';
|
||||||
|
import * as messages from '../../../../shared/messages';
|
||||||
|
import * as supportedFormats from '../../../../shared/supported-formats';
|
||||||
|
import * as selectionState from '../../models/selection-state';
|
||||||
|
import { observe } from '../../models/store';
|
||||||
|
import * as analytics from '../../modules/analytics';
|
||||||
|
import * as exceptionReporter from '../../modules/exception-reporter';
|
||||||
|
import * as osDialog from '../../os/dialog';
|
||||||
|
import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives';
|
||||||
|
import {
|
||||||
|
ChangeButton,
|
||||||
|
DetailsText,
|
||||||
|
Modal,
|
||||||
|
StepButton,
|
||||||
|
StepNameButton,
|
||||||
|
ScrollableFlex,
|
||||||
|
} from '../../styled-components';
|
||||||
|
import { colors } from '../../theme';
|
||||||
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||||
|
|
||||||
|
import ImageSvg from '../../../assets/image.svg';
|
||||||
|
import SrcSvg from '../../../assets/src.svg';
|
||||||
|
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||||
|
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||||
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
import { isJson } from '../../../../shared/utils';
|
||||||
|
|
||||||
|
const recentUrlImagesKey = 'recentUrlImages';
|
||||||
|
|
||||||
|
function normalizeRecentUrlImages(urls: any[]): URL[] {
|
||||||
|
if (!Array.isArray(urls)) {
|
||||||
|
urls = [];
|
||||||
|
}
|
||||||
|
urls = urls
|
||||||
|
.map((url) => {
|
||||||
|
try {
|
||||||
|
return new URL(url);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Invalid URL, skip
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((url) => url !== undefined);
|
||||||
|
urls = _.uniqBy(urls, (url) => url.href);
|
||||||
|
return urls.slice(urls.length - 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecentUrlImages(): URL[] {
|
||||||
|
let urls = [];
|
||||||
|
try {
|
||||||
|
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
return normalizeRecentUrlImages(urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRecentUrlImages(urls: URL[]) {
|
||||||
|
const normalized = normalizeRecentUrlImages(urls.map((url: URL) => url.href));
|
||||||
|
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
const isURL = (imagePath: string) =>
|
||||||
|
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||||
|
|
||||||
|
const Card = styled(BaseCard)`
|
||||||
|
hr {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// TODO move these styles to rendition
|
||||||
|
const ModalText = styled.p`
|
||||||
|
a {
|
||||||
|
color: rgb(0, 174, 239);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgb(0, 139, 191);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function getState() {
|
||||||
|
const image = selectionState.getImage();
|
||||||
|
return {
|
||||||
|
hasImage: selectionState.hasImage(),
|
||||||
|
imageName: image?.name,
|
||||||
|
imageSize: image?.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isString(value: any): value is string {
|
||||||
|
return typeof value === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
const URLSelector = ({
|
||||||
|
done,
|
||||||
|
cancel,
|
||||||
|
}: {
|
||||||
|
done: (imageURL: string, auth?: Authentication) => void;
|
||||||
|
cancel: () => void;
|
||||||
|
}) => {
|
||||||
|
const [imageURL, setImageURL] = React.useState('');
|
||||||
|
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
|
||||||
|
const [username, setUsername] = React.useState('');
|
||||||
|
const [password, setPassword] = React.useState('');
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchRecentUrlImages = async () => {
|
||||||
|
const recentUrlImages: URL[] = await getRecentUrlImages();
|
||||||
|
setRecentImages(recentUrlImages);
|
||||||
|
};
|
||||||
|
fetchRecentUrlImages();
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
cancel={cancel}
|
||||||
|
primaryButtonProps={{
|
||||||
|
disabled: loading || !imageURL,
|
||||||
|
}}
|
||||||
|
action={loading ? <Spinner /> : 'OK'}
|
||||||
|
done={async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const urlStrings = recentImages.map((url: URL) => url.href);
|
||||||
|
const normalizedRecentUrls = normalizeRecentUrlImages([
|
||||||
|
...urlStrings,
|
||||||
|
imageURL,
|
||||||
|
]);
|
||||||
|
setRecentUrlImages(normalizedRecentUrls);
|
||||||
|
const auth = username ? { username, password } : undefined;
|
||||||
|
await done(imageURL, auth);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
|
||||||
|
<Txt mb="10px" fontSize="24px">
|
||||||
|
Use Image URL
|
||||||
|
</Txt>
|
||||||
|
<Input
|
||||||
|
value={imageURL}
|
||||||
|
placeholder="Enter a valid URL"
|
||||||
|
type="text"
|
||||||
|
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setImageURL(evt.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
mt={15}
|
||||||
|
mb={15}
|
||||||
|
fontSize="14px"
|
||||||
|
onClick={() => {
|
||||||
|
if (showBasicAuth) {
|
||||||
|
setUsername('');
|
||||||
|
setPassword('');
|
||||||
|
}
|
||||||
|
setShowBasicAuth(!showBasicAuth);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
{showBasicAuth && (
|
||||||
|
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||||
|
)}
|
||||||
|
{!showBasicAuth && (
|
||||||
|
<ChevronRightSvg height="1em" fill="currentColor" />
|
||||||
|
)}
|
||||||
|
<Txt ml={8}>Authentication</Txt>
|
||||||
|
</Flex>
|
||||||
|
</Link>
|
||||||
|
{showBasicAuth && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Input
|
||||||
|
mb={15}
|
||||||
|
value={username}
|
||||||
|
placeholder="Enter username"
|
||||||
|
type="text"
|
||||||
|
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setUsername(evt.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={password}
|
||||||
|
placeholder="Enter password"
|
||||||
|
type="password"
|
||||||
|
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPassword(evt.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{recentImages.length > 0 && (
|
||||||
|
<Flex flexDirection="column" height="78.6%">
|
||||||
|
<Txt fontSize={18}>Recent</Txt>
|
||||||
|
<ScrollableFlex flexDirection="column">
|
||||||
|
<Card
|
||||||
|
p="10px 15px"
|
||||||
|
rows={recentImages
|
||||||
|
.map((recent) => (
|
||||||
|
<Txt
|
||||||
|
key={recent.href}
|
||||||
|
onClick={() => {
|
||||||
|
setImageURL(recent.href);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{recent.pathname.split('/').pop()} - {recent.href}
|
||||||
|
</Txt>
|
||||||
|
))
|
||||||
|
.reverse()}
|
||||||
|
/>
|
||||||
|
</ScrollableFlex>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Flow {
|
||||||
|
icon?: JSX.Element;
|
||||||
|
onClick: (evt: React.MouseEvent) => void;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlowSelector = styled(
|
||||||
|
({ flow, ...props }: { flow: Flow } & ButtonProps) => (
|
||||||
|
<StepButton
|
||||||
|
plain={!props.primary}
|
||||||
|
primary={props.primary}
|
||||||
|
onClick={(evt: React.MouseEvent<Element, MouseEvent>) =>
|
||||||
|
flow.onClick(evt)
|
||||||
|
}
|
||||||
|
icon={flow.icon}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{flow.label}
|
||||||
|
</StepButton>
|
||||||
|
),
|
||||||
|
)`
|
||||||
|
border-radius: 24px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
|
||||||
|
:enabled:focus,
|
||||||
|
:enabled:focus svg {
|
||||||
|
color: ${colors.primary.foreground} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:enabled:hover {
|
||||||
|
background-color: ${colors.primary.background};
|
||||||
|
color: ${colors.primary.foreground};
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: ${colors.primary.foreground}!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type Source =
|
||||||
|
| typeof sourceDestination.File
|
||||||
|
| typeof sourceDestination.BlockDevice
|
||||||
|
| typeof sourceDestination.Http;
|
||||||
|
|
||||||
|
export interface SourceMetadata extends sourceDestination.Metadata {
|
||||||
|
hasMBR?: boolean;
|
||||||
|
partitions?: MBRPartition[] | GPTPartition[];
|
||||||
|
path: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
SourceType: Source;
|
||||||
|
drive?: DrivelistDrive;
|
||||||
|
extension?: string;
|
||||||
|
archiveExtension?: string;
|
||||||
|
auth?: Authentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SourceSelectorProps {
|
||||||
|
flashing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SourceSelectorState {
|
||||||
|
hasImage: boolean;
|
||||||
|
imageName?: string;
|
||||||
|
imageSize?: number;
|
||||||
|
warning: { message: string; title: string | null } | null;
|
||||||
|
showImageDetails: boolean;
|
||||||
|
showURLSelector: boolean;
|
||||||
|
showDriveSelector: boolean;
|
||||||
|
defaultFlowActive: boolean;
|
||||||
|
imageSelectorOpen: boolean;
|
||||||
|
imageLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Authentication {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SourceSelector extends React.Component<
|
||||||
|
SourceSelectorProps,
|
||||||
|
SourceSelectorState
|
||||||
|
> {
|
||||||
|
private unsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
|
constructor(props: SourceSelectorProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
...getState(),
|
||||||
|
warning: null,
|
||||||
|
showImageDetails: false,
|
||||||
|
showURLSelector: false,
|
||||||
|
showDriveSelector: false,
|
||||||
|
defaultFlowActive: true,
|
||||||
|
imageSelectorOpen: false,
|
||||||
|
imageLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind `this` since it's used in an event's callback
|
||||||
|
this.onSelectImage = this.onSelectImage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.unsubscribe = observe(() => {
|
||||||
|
this.setState(getState());
|
||||||
|
});
|
||||||
|
ipcRenderer.on('select-image', this.onSelectImage);
|
||||||
|
ipcRenderer.send('source-selector-ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.unsubscribe?.();
|
||||||
|
ipcRenderer.removeListener('select-image', this.onSelectImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||||
|
this.setState({ imageLoading: true });
|
||||||
|
await this.selectSource(
|
||||||
|
imagePath,
|
||||||
|
isURL(this.normalizeImagePath(imagePath))
|
||||||
|
? sourceDestination.Http
|
||||||
|
: sourceDestination.File,
|
||||||
|
).promise;
|
||||||
|
this.setState({ imageLoading: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createSource(
|
||||||
|
selected: string,
|
||||||
|
SourceType: Source,
|
||||||
|
auth?: Authentication,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
selected = await replaceWindowsNetworkDriveLetter(selected);
|
||||||
|
} catch (error: any) {
|
||||||
|
analytics.logException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJson(decodeURIComponent(selected))) {
|
||||||
|
const config: AxiosRequestConfig = JSON.parse(
|
||||||
|
decodeURIComponent(selected),
|
||||||
|
);
|
||||||
|
return new sourceDestination.Http({
|
||||||
|
url: config.url!,
|
||||||
|
axiosInstance: axios.create(_.omit(config, ['url'])),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SourceType === sourceDestination.File) {
|
||||||
|
return new sourceDestination.File({
|
||||||
|
path: selected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new sourceDestination.Http({ url: selected, auth });
|
||||||
|
}
|
||||||
|
|
||||||
|
public normalizeImagePath(imgPath: string) {
|
||||||
|
const decodedPath = decodeURIComponent(imgPath);
|
||||||
|
if (isJson(decodedPath)) {
|
||||||
|
return JSON.parse(decodedPath).url ?? decodedPath;
|
||||||
|
}
|
||||||
|
return decodedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private reselectSource() {
|
||||||
|
analytics.logEvent('Reselect image', {
|
||||||
|
previousImage: selectionState.getImage(),
|
||||||
|
});
|
||||||
|
|
||||||
|
selectionState.deselectImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectSource(
|
||||||
|
selected: string | DrivelistDrive,
|
||||||
|
SourceType: Source,
|
||||||
|
auth?: Authentication,
|
||||||
|
): { promise: Promise<void>; cancel: () => void } {
|
||||||
|
let cancelled = false;
|
||||||
|
return {
|
||||||
|
cancel: () => {
|
||||||
|
cancelled = true;
|
||||||
|
},
|
||||||
|
promise: (async () => {
|
||||||
|
const sourcePath = isString(selected) ? selected : selected.device;
|
||||||
|
let source;
|
||||||
|
let metadata: SourceMetadata | undefined;
|
||||||
|
if (isString(selected)) {
|
||||||
|
if (
|
||||||
|
SourceType === sourceDestination.Http &&
|
||||||
|
!isURL(this.normalizeImagePath(selected))
|
||||||
|
) {
|
||||||
|
this.handleError(
|
||||||
|
'Unsupported protocol',
|
||||||
|
selected,
|
||||||
|
messages.error.unsupportedProtocol(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
||||||
|
analytics.logEvent('Possibly Windows image', { image: selected });
|
||||||
|
this.setState({
|
||||||
|
warning: {
|
||||||
|
message: messages.warning.looksLikeWindowsImage(),
|
||||||
|
title: 'Possible Windows image detected',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
source = await this.createSource(selected, SourceType, auth);
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const innerSource = await source.getInnerSource();
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metadata = await this.getMetadata(innerSource, selected);
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metadata.SourceType = SourceType;
|
||||||
|
|
||||||
|
if (!metadata.hasMBR && this.state.warning === null) {
|
||||||
|
analytics.logEvent('Missing partition table', { metadata });
|
||||||
|
this.setState({
|
||||||
|
warning: {
|
||||||
|
message: messages.warning.missingPartitionTable(),
|
||||||
|
title: 'Missing partition table',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.handleError(
|
||||||
|
'Error opening source',
|
||||||
|
sourcePath,
|
||||||
|
messages.error.openSource(sourcePath, error.message),
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await source.close();
|
||||||
|
} catch (error: any) {
|
||||||
|
// Noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selected.partitionTableType === null) {
|
||||||
|
analytics.logEvent('Missing partition table', { selected });
|
||||||
|
this.setState({
|
||||||
|
warning: {
|
||||||
|
message: messages.warning.driveMissingPartitionTable(),
|
||||||
|
title: 'Missing partition table',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
metadata = {
|
||||||
|
path: selected.device,
|
||||||
|
displayName: selected.displayName,
|
||||||
|
description: selected.displayName,
|
||||||
|
size: selected.size as SourceMetadata['size'],
|
||||||
|
SourceType: sourceDestination.BlockDevice,
|
||||||
|
drive: selected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata !== undefined) {
|
||||||
|
metadata.auth = auth;
|
||||||
|
selectionState.selectSource(metadata);
|
||||||
|
analytics.logEvent('Select image', {
|
||||||
|
// An easy way so we can quickly identify if we're making use of
|
||||||
|
// certain features without printing pages of text to DevTools.
|
||||||
|
image: {
|
||||||
|
...metadata,
|
||||||
|
logo: Boolean(metadata.logo),
|
||||||
|
blockMap: Boolean(metadata.blockMap),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(
|
||||||
|
title: string,
|
||||||
|
sourcePath: string,
|
||||||
|
description: string,
|
||||||
|
error?: Error,
|
||||||
|
) {
|
||||||
|
const imageError = errors.createUserError({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
osDialog.showError(imageError);
|
||||||
|
if (error) {
|
||||||
|
analytics.logException(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
analytics.logEvent(title, { path: sourcePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getMetadata(
|
||||||
|
source: sourceDestination.SourceDestination,
|
||||||
|
selected: string | DrivelistDrive,
|
||||||
|
) {
|
||||||
|
const metadata = (await source.getMetadata()) as SourceMetadata;
|
||||||
|
const partitionTable = await source.getPartitionTable();
|
||||||
|
if (partitionTable) {
|
||||||
|
metadata.hasMBR = true;
|
||||||
|
metadata.partitions = partitionTable.partitions;
|
||||||
|
} else {
|
||||||
|
metadata.hasMBR = false;
|
||||||
|
}
|
||||||
|
if (isString(selected)) {
|
||||||
|
metadata.extension = path.extname(selected).slice(1);
|
||||||
|
metadata.path = selected;
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openImageSelector() {
|
||||||
|
analytics.logEvent('Open image selector');
|
||||||
|
this.setState({ imageSelectorOpen: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imagePath = await osDialog.selectImage();
|
||||||
|
// Avoid analytics and selection state changes
|
||||||
|
// if no file was resolved from the dialog.
|
||||||
|
if (!imagePath) {
|
||||||
|
analytics.logEvent('Image selector closed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.selectSource(imagePath, sourceDestination.File).promise;
|
||||||
|
} catch (error: any) {
|
||||||
|
exceptionReporter.report(error);
|
||||||
|
} finally {
|
||||||
|
this.setState({ imageSelectorOpen: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
|
||||||
|
const [file] = event.dataTransfer.files;
|
||||||
|
if (file) {
|
||||||
|
await this.selectSource(file.path, sourceDestination.File).promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private openURLSelector() {
|
||||||
|
analytics.logEvent('Open image URL selector');
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showURLSelector: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private openDriveSelector() {
|
||||||
|
analytics.logEvent('Open drive selector');
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showDriveSelector: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
|
||||||
|
// Needed to get onDrop events on div elements
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDragEnter(event: React.DragEvent<HTMLDivElement>) {
|
||||||
|
// Needed to get onDrop events on div elements
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private showSelectedImageDetails() {
|
||||||
|
analytics.logEvent('Show selected image tooltip', {
|
||||||
|
imagePath: selectionState.getImage()?.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showImageDetails: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setDefaultFlowActive(defaultFlowActive: boolean) {
|
||||||
|
this.setState({ defaultFlowActive });
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeModal() {
|
||||||
|
this.setState({
|
||||||
|
showDriveSelector: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add a visual change when dragging a file over the selector
|
||||||
|
public render() {
|
||||||
|
const { flashing } = this.props;
|
||||||
|
const {
|
||||||
|
showImageDetails,
|
||||||
|
showURLSelector,
|
||||||
|
showDriveSelector,
|
||||||
|
imageLoading,
|
||||||
|
} = this.state;
|
||||||
|
const selectionImage = selectionState.getImage();
|
||||||
|
let image: SourceMetadata | DrivelistDrive =
|
||||||
|
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
||||||
|
|
||||||
|
image = image.drive ?? image;
|
||||||
|
|
||||||
|
let cancelURLSelection = () => {
|
||||||
|
// noop
|
||||||
|
};
|
||||||
|
image.name = image.description || image.name;
|
||||||
|
const imagePath = image.path || image.displayName || '';
|
||||||
|
const imageBasename = path.basename(imagePath);
|
||||||
|
const imageName = image.name || '';
|
||||||
|
const imageSize = image.size;
|
||||||
|
const imageLogo = image.logo || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
|
||||||
|
onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||||
|
this.onDragEnter(evt)
|
||||||
|
}
|
||||||
|
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||||
|
this.onDragOver(evt)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SVGIcon
|
||||||
|
contents={imageLogo}
|
||||||
|
fallback={ImageSvg}
|
||||||
|
style={{
|
||||||
|
marginBottom: 30,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectionImage !== undefined || imageLoading ? (
|
||||||
|
<>
|
||||||
|
<StepNameButton
|
||||||
|
plain
|
||||||
|
onClick={() => this.showSelectedImageDetails()}
|
||||||
|
tooltip={imageName || imageBasename}
|
||||||
|
>
|
||||||
|
<Spinner show={imageLoading}>
|
||||||
|
{middleEllipsis(imageName || imageBasename, 20)}
|
||||||
|
</Spinner>
|
||||||
|
</StepNameButton>
|
||||||
|
{!flashing && !imageLoading && (
|
||||||
|
<ChangeButton
|
||||||
|
plain
|
||||||
|
mb={14}
|
||||||
|
onClick={() => this.reselectSource()}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</ChangeButton>
|
||||||
|
)}
|
||||||
|
{!_.isNil(imageSize) && !imageLoading && (
|
||||||
|
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FlowSelector
|
||||||
|
disabled={this.state.imageSelectorOpen}
|
||||||
|
primary={this.state.defaultFlowActive}
|
||||||
|
key="Flash from file"
|
||||||
|
flow={{
|
||||||
|
onClick: () => this.openImageSelector(),
|
||||||
|
label: 'Flash from file',
|
||||||
|
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||||
|
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||||
|
/>
|
||||||
|
<FlowSelector
|
||||||
|
key="Flash from URL"
|
||||||
|
flow={{
|
||||||
|
onClick: () => this.openURLSelector(),
|
||||||
|
label: 'Flash from URL',
|
||||||
|
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||||
|
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||||
|
/>
|
||||||
|
<FlowSelector
|
||||||
|
key="Clone drive"
|
||||||
|
flow={{
|
||||||
|
onClick: () => this.openDriveSelector(),
|
||||||
|
label: 'Clone drive',
|
||||||
|
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||||
|
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{this.state.warning != null && (
|
||||||
|
<SmallModal
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
|
||||||
|
}}
|
||||||
|
titleElement={
|
||||||
|
<span>
|
||||||
|
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
|
||||||
|
<span>{this.state.warning.title}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
action="Continue"
|
||||||
|
cancel={() => {
|
||||||
|
this.setState({ warning: null });
|
||||||
|
this.reselectSource();
|
||||||
|
}}
|
||||||
|
done={() => {
|
||||||
|
this.setState({ warning: null });
|
||||||
|
}}
|
||||||
|
primaryButtonProps={{ warning: true, primary: false }}
|
||||||
|
>
|
||||||
|
<ModalText
|
||||||
|
dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
|
||||||
|
/>
|
||||||
|
</SmallModal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showImageDetails && (
|
||||||
|
<SmallModal
|
||||||
|
title="Image"
|
||||||
|
done={() => {
|
||||||
|
this.setState({ showImageDetails: false });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Txt.p>
|
||||||
|
<Txt.span bold>Name: </Txt.span>
|
||||||
|
<Txt.span>{imageName || imageBasename}</Txt.span>
|
||||||
|
</Txt.p>
|
||||||
|
<Txt.p>
|
||||||
|
<Txt.span bold>Path: </Txt.span>
|
||||||
|
<Txt.span>{imagePath}</Txt.span>
|
||||||
|
</Txt.p>
|
||||||
|
</SmallModal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showURLSelector && (
|
||||||
|
<URLSelector
|
||||||
|
cancel={() => {
|
||||||
|
cancelURLSelection();
|
||||||
|
this.setState({
|
||||||
|
showURLSelector: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
done={async (imageURL: string, auth?: Authentication) => {
|
||||||
|
// Avoid analytics and selection state changes
|
||||||
|
// if no file was resolved from the dialog.
|
||||||
|
if (!imageURL) {
|
||||||
|
analytics.logEvent('URL selector closed');
|
||||||
|
} else {
|
||||||
|
let promise;
|
||||||
|
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||||
|
imageURL,
|
||||||
|
sourceDestination.Http,
|
||||||
|
auth,
|
||||||
|
));
|
||||||
|
await promise;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
showURLSelector: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDriveSelector && (
|
||||||
|
<DriveSelector
|
||||||
|
write={false}
|
||||||
|
multipleSelection={false}
|
||||||
|
titleLabel="Select source"
|
||||||
|
emptyListLabel="Plug a source drive"
|
||||||
|
emptyListIcon={<SrcSvg width="40px" />}
|
||||||
|
cancel={(originalList) => {
|
||||||
|
if (originalList.length) {
|
||||||
|
const originalSource = originalList[0];
|
||||||
|
if (selectionImage?.drive?.device !== originalSource.device) {
|
||||||
|
this.selectSource(
|
||||||
|
originalSource,
|
||||||
|
sourceDestination.BlockDevice,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectionState.deselectImage();
|
||||||
|
}
|
||||||
|
this.closeModal();
|
||||||
|
}}
|
||||||
|
done={() => this.closeModal()}
|
||||||
|
onSelect={(drive) => {
|
||||||
|
if (drive) {
|
||||||
|
if (
|
||||||
|
selectionState.getImage()?.drive?.device === drive?.device
|
||||||
|
) {
|
||||||
|
return selectionState.deselectImage();
|
||||||
|
}
|
||||||
|
this.selectSource(drive, sourceDestination.BlockDevice);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/* eslint-disable jsdoc/require-example */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.SVGIcon
|
|
||||||
*/
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const angular = require('angular')
|
|
||||||
const react = require('react')
|
|
||||||
const propTypes = require('prop-types')
|
|
||||||
const react2angular = require('react2angular').react2angular
|
|
||||||
const path = require('path')
|
|
||||||
const fs = require('fs')
|
|
||||||
const analytics = require('../modules/analytics')
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.SVGIcon'
|
|
||||||
const angularSVGIcon = angular.module(MODULE_NAME, [])
|
|
||||||
|
|
||||||
const DEFAULT_SIZE = '40px'
|
|
||||||
|
|
||||||
const domParser = new window.DOMParser()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Try to parse SVG contents and return it data encoded
|
|
||||||
*
|
|
||||||
* @param {String} contents - SVG XML contents
|
|
||||||
* @returns {String|null}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const encodedSVG = tryParseSVGContents('<svg><path></path></svg>')
|
|
||||||
*
|
|
||||||
* img.src = encodedSVG
|
|
||||||
*/
|
|
||||||
const tryParseSVGContents = (contents) => {
|
|
||||||
const doc = domParser.parseFromString(contents, 'image/svg+xml')
|
|
||||||
const parserError = doc.querySelector('parsererror')
|
|
||||||
const svg = doc.querySelector('svg')
|
|
||||||
|
|
||||||
if (!parserError && svg) {
|
|
||||||
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary SVG element that takes both filepaths and file contents
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
class SVGIcon extends react.Component {
|
|
||||||
/**
|
|
||||||
* @summary Render the SVG
|
|
||||||
* @returns {react.Element}
|
|
||||||
*/
|
|
||||||
render () {
|
|
||||||
// __dirname behaves strangely inside a Webpack bundle,
|
|
||||||
// so we need to provide different base directories
|
|
||||||
// depending on whether __dirname is absolute or not,
|
|
||||||
// which helps detecting a Webpack bundle.
|
|
||||||
// We use global.__dirname inside a Webpack bundle since
|
|
||||||
// that's the only way to get the "real" __dirname.
|
|
||||||
const baseDirectory = path.isAbsolute(__dirname)
|
|
||||||
? path.join(__dirname, '..')
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
: global.__dirname
|
|
||||||
|
|
||||||
let svgData = ''
|
|
||||||
|
|
||||||
_.find(this.props.contents, (content) => {
|
|
||||||
const attempt = tryParseSVGContents(content)
|
|
||||||
|
|
||||||
if (attempt) {
|
|
||||||
svgData = attempt
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!svgData) {
|
|
||||||
_.find(this.props.paths, (relativePath) => {
|
|
||||||
// This means the path to the icon should be
|
|
||||||
// relative to *this directory*.
|
|
||||||
// TODO: There might be a way to compute the path
|
|
||||||
// relatively to the `index.html`.
|
|
||||||
const imagePath = path.join(baseDirectory, 'assets', relativePath)
|
|
||||||
|
|
||||||
const contents = _.attempt(() => {
|
|
||||||
return fs.readFileSync(imagePath, {
|
|
||||||
encoding: 'utf8'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (_.isError(contents)) {
|
|
||||||
analytics.logException(contents)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = _.attempt(tryParseSVGContents, contents)
|
|
||||||
|
|
||||||
if (parsed) {
|
|
||||||
svgData = parsed
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const width = this.props.width || DEFAULT_SIZE
|
|
||||||
const height = this.props.height || DEFAULT_SIZE
|
|
||||||
|
|
||||||
return react.createElement('img', {
|
|
||||||
className: 'svg-icon',
|
|
||||||
style: {
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
},
|
|
||||||
src: svgData,
|
|
||||||
disabled: this.props.disabled
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Cause a re-render due to changed element properties
|
|
||||||
* @param {Object} nextProps - the new properties
|
|
||||||
*/
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
// This will update the element if the properties change
|
|
||||||
this.setState(nextProps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SVGIcon.propTypes = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Paths to SVG files to be tried in succession if any fails
|
|
||||||
*/
|
|
||||||
paths: propTypes.array,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary List of embedded SVG contents to be tried in succession if any fails
|
|
||||||
*/
|
|
||||||
contents: propTypes.array,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary SVG image width unit
|
|
||||||
*/
|
|
||||||
width: propTypes.string,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary SVG image height unit
|
|
||||||
*/
|
|
||||||
height: propTypes.string,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Should the element visually appear grayed out and disabled?
|
|
||||||
*/
|
|
||||||
disabled: propTypes.bool
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
angularSVGIcon.component('svgIcon', react2angular(SVGIcon))
|
|
||||||
module.exports = MODULE_NAME
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
svg-icon {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||