Compare commits
1103 Commits
v1.4.3
...
save-url-i
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f6ce9a217d | ||
![]() |
fce2d94df7 | ||
![]() |
3feb22ee66 | ||
![]() |
b80a6b2feb | ||
![]() |
b4e6970119 | ||
![]() |
2e3978b3c9 | ||
![]() |
c6cd421f17 | ||
![]() |
c3296eed54 | ||
![]() |
153e37b9dc | ||
![]() |
78aca6a19f | ||
![]() |
27695babfd | ||
![]() |
06a96db72d | ||
![]() |
6584cef774 | ||
![]() |
3c77800b1d | ||
![]() |
74a78076cf | ||
![]() |
8ff8b02f37 | ||
![]() |
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 |
@@ -14,3 +14,9 @@ trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.ts]
|
||||
indent_style = tab
|
||||
|
||||
[*.tsx]
|
||||
indent_style = tab
|
||||
|
@@ -8,9 +8,12 @@ plugins:
|
||||
- lodash
|
||||
- jsdoc
|
||||
- node
|
||||
- react
|
||||
extends: 'standard'
|
||||
parserOptions:
|
||||
sourceType: 'script'
|
||||
ecmaFeatures:
|
||||
jsx: true
|
||||
settings:
|
||||
jsdoc:
|
||||
additionalTagNames:
|
||||
@@ -286,7 +289,7 @@ rules:
|
||||
- error
|
||||
- anonymous: always
|
||||
named: always
|
||||
asyncArrow: never
|
||||
asyncArrow: always
|
||||
template-tag-spacing:
|
||||
- error
|
||||
- always
|
||||
@@ -295,9 +298,6 @@ rules:
|
||||
|
||||
# ECMAScript 6
|
||||
|
||||
arrow-body-style:
|
||||
- error
|
||||
- always
|
||||
arrow-parens:
|
||||
- error
|
||||
- always
|
||||
@@ -318,8 +318,6 @@ rules:
|
||||
- always
|
||||
prefer-const:
|
||||
- error
|
||||
prefer-reflect:
|
||||
- error
|
||||
prefer-spread:
|
||||
- error
|
||||
prefer-numeric-literals:
|
||||
@@ -444,3 +442,13 @@ rules:
|
||||
node/no-extraneous-import:
|
||||
- error
|
||||
|
||||
# React
|
||||
|
||||
react/jsx-uses-vars:
|
||||
- error
|
||||
|
||||
overrides:
|
||||
files: ['*.jsx']
|
||||
rules:
|
||||
require-jsdoc:
|
||||
- off
|
||||
|
14
.gitattributes
vendored
@@ -1,5 +1,8 @@
|
||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||
*.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 text eol=lf
|
||||
*.scss text eol=lf
|
||||
@@ -11,7 +14,7 @@ Dockerfile* text
|
||||
etcher text
|
||||
.git* text
|
||||
*.html text
|
||||
*.json text
|
||||
*.json text eol=lf
|
||||
*.cpp text
|
||||
*.h text
|
||||
*.gyp text
|
||||
@@ -24,6 +27,8 @@ Makefile text
|
||||
*.yml text
|
||||
*.patch text
|
||||
*.txt text
|
||||
CODEOWNERS text
|
||||
*.plist text
|
||||
|
||||
# Binary files (no line-ending conversions)
|
||||
*.bz2 binary diff=hex
|
||||
@@ -44,5 +49,12 @@ Makefile text
|
||||
*.bin binary diff=hex
|
||||
*.dmg binary diff=hex
|
||||
*.rpi-sdcard binary diff=hex
|
||||
*.wic binary diff=hex
|
||||
*.foo binary diff=hex
|
||||
*.eot binary diff=hex
|
||||
*.otf binary diff=hex
|
||||
*.woff binary diff=hex
|
||||
*.woff2 binary diff=hex
|
||||
*.ttf binary diff=hex
|
||||
xz-without-extension binary diff=hex
|
||||
wmic-output.txt binary diff=hex
|
||||
|
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -3,4 +3,4 @@
|
||||
- **Image flashed:**
|
||||
- **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+Alt+I` if you're on Mac OS. -->
|
||||
<!-- 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. -->
|
||||
|
8
.gitignore
vendored
@@ -43,3 +43,11 @@ node_modules
|
||||
*.cer
|
||||
*.crt
|
||||
*.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": {
|
||||
"npm_version": "6.14.5",
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
"libusb-1.0-0-dev",
|
||||
"libyaml-dev"
|
||||
"libyaml-dev",
|
||||
"libgtk-3-0",
|
||||
"libatk-bridge2.0-0",
|
||||
"libdbus-1-3",
|
||||
"libgbm1",
|
||||
"libc6"
|
||||
]
|
||||
},
|
||||
"builder": {
|
||||
"appId": "io.resin.etcher",
|
||||
"copyright": "Copyright 2016-2018 Resinio Ltd",
|
||||
"productName": "Etcher",
|
||||
"nodeGypRebuild": true,
|
||||
"appId": "io.balena.etcher",
|
||||
"copyright": "Copyright 2016-2020 Balena Ltd",
|
||||
"productName": "balenaEtcher",
|
||||
"nodeGypRebuild": false,
|
||||
"afterPack": "./afterPack.js",
|
||||
"asar": false,
|
||||
"files": [
|
||||
"!lib/gui/app",
|
||||
"lib/gui/app/index.html",
|
||||
"generated"
|
||||
"generated",
|
||||
"lib/shared/catalina-sudo/sudo-askpass.osascript.js"
|
||||
],
|
||||
"beforeBuild": "./beforeBuild.js",
|
||||
"afterSign": "./afterSignHook.js",
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools"
|
||||
"category": "public.app-category.developer-tools",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "entitlements.mac.plist",
|
||||
"entitlementsInherit": "entitlements.mac.plist"
|
||||
},
|
||||
"dmg": {
|
||||
"iconSize": 110,
|
||||
"contents": [
|
||||
{
|
||||
"x": 140,
|
||||
"y": 225
|
||||
"y": 245
|
||||
},
|
||||
{
|
||||
"x": 415,
|
||||
"y": 225,
|
||||
"y": 245,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"width": 540,
|
||||
"height": 405
|
||||
"width": 544,
|
||||
"height": 407
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
"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": {
|
||||
"compression": "bzip2",
|
||||
"priority": "optional",
|
||||
"depends": [
|
||||
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
|
||||
]
|
||||
},
|
||||
"protocols": {
|
||||
"name": "etcher",
|
||||
"schemes": [
|
||||
"etcher"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +0,0 @@
|
||||
# sass-lint config generated by make-sass-lint-config v0.1.2
|
||||
|
||||
files:
|
||||
include: lib/gui/scss/**/*.scss
|
||||
options:
|
||||
formatter: stylish
|
||||
merge-default-rules: false
|
||||
rules:
|
||||
no-css-comments: 0
|
||||
no-important: 0
|
||||
no-qualifying-elements: 0
|
||||
placeholder-in-extend: 0
|
||||
property-sort-order: 0
|
||||
quotes:
|
||||
- 1
|
||||
- style: double
|
||||
|
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
|
819
CHANGELOG.md
@@ -3,6 +3,825 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# v1.5.109
|
||||
## (2020-09-14)
|
||||
|
||||
* Workaround elevation bug on Windows when the username contains an ampersand [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.108
|
||||
## (2020-09-10)
|
||||
|
||||
* Fix content not loading when the app path contains special characters [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.107
|
||||
## (2020-09-04)
|
||||
|
||||
* Re-enable ext partitions trimming on 32 bit Windows [Alexis Svinartchouk]
|
||||
* Rework system & large drives handling logic [Lorenzo Alberto Maria Ambrosi]
|
||||
* Reword macOS Catalina askpass message [Lorenzo Alberto Maria Ambrosi]
|
||||
* Add clone-drive workflow [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.106
|
||||
## (2020-08-27)
|
||||
|
||||
* Disable ext partitions trimming on 32 bit windows until it is fixed [Alexis Svinartchouk]
|
||||
* Fix opening zip files from servers accepting Range headers [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.105
|
||||
## (2020-08-25)
|
||||
|
||||
* Update etcher-sdk to 4.1.26 [Alexis Svinartchouk]
|
||||
* URL selector cancel button cancels ongoing url selection [Alexis Svinartchouk]
|
||||
* Spinner for URL selector modal [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.104
|
||||
## (2020-08-20)
|
||||
|
||||
* Fix writing config file [Alexis Svinartchouk]
|
||||
* Update electron to v9.2.1 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.103
|
||||
## (2020-08-18)
|
||||
|
||||
* Update rendition to ^17 [Alexis Svinartchouk]
|
||||
* Update electron to 9.2.0 [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to ^4.1.23 [Alexis Svinartchouk]
|
||||
* Move linting and testing into package.json [Alexis Svinartchouk]
|
||||
* Set module: es2015 in tsconfig.json [Alexis Svinartchouk]
|
||||
* Replace native elevator with sudo-prompt on windows [Alexis Svinartchouk]
|
||||
* Don't import WeakMap polyfill in deep-map-keys [Alexis Svinartchouk]
|
||||
* Don't use lodash in child-writer.js [Alexis Svinartchouk]
|
||||
* Optimize svgs [Alexis Svinartchouk]
|
||||
* User regular stream in lzma-native instead of readable-stream [Alexis Svinartchouk]
|
||||
* Remove Bluebird [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.102
|
||||
## (2020-07-27)
|
||||
|
||||
* Fix flashing truncated images, fix flashing large dmgs [Alexis Svinartchouk]
|
||||
* Electron 9.1.1 [Alexis Svinartchouk]
|
||||
* Remove bluebird from main process, reduce lodash usage [Alexis Svinartchouk]
|
||||
* Centralize imports in child-writer [Alexis Svinartchouk]
|
||||
* Split main process and child-writer js files [Alexis Svinartchouk]
|
||||
* Stop using request, replace it with already used axios [Alexis Svinartchouk]
|
||||
* Remove font awesome unused icons from the generated bundle [Alexis Svinartchouk]
|
||||
* Remove no longer used .sass-lint.yml [Alexis Svinartchouk]
|
||||
* Use tslib [Alexis Svinartchouk]
|
||||
* Use strict typescript compiler option [Alexis Svinartchouk]
|
||||
* Update rendition to ^16.1.1 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.101
|
||||
## (2020-07-09)
|
||||
|
||||
* Resize modal to show content appropriately [Lorenzo Alberto Maria Ambrosi]
|
||||
* Update etcher-sdk to v4.1.16 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Convert sass to plain css [Lorenzo Alberto Maria Ambrosi]
|
||||
* Remove unused scss [Lorenzo Alberto Maria Ambrosi]
|
||||
* Remove unused warning in settings [Lorenzo Alberto Maria Ambrosi]
|
||||
* Refactor UI without bootstrap & flexboxgrid [Lorenzo Alberto Maria Ambrosi]
|
||||
* Restyle modals [Lorenzo Alberto Maria Ambrosi]
|
||||
* Remove bootstrap & flexboxgrid [Lorenzo Alberto Maria Ambrosi]
|
||||
* Rework and move flashing view elements [Lorenzo Alberto Maria Ambrosi]
|
||||
* Refactor UI grid to use rendition [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.100
|
||||
## (2020-06-22)
|
||||
|
||||
* Update partitioninfo to 5.3.5 [Alexis Svinartchouk]
|
||||
* Add .vhd to the list of supported extensions, allow opening any file [Alexis Svinartchouk]
|
||||
* Update mocha to v8.0.1 [Alexis Svinartchouk]
|
||||
* Update electron-notarize to v1.0.0 [Alexis Svinartchouk]
|
||||
* Update electron to v9.0.4 [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to v4.1.15 [Alexis Svinartchouk]
|
||||
* Sticky header in target selection table [Alexis Svinartchouk]
|
||||
* Update rendition to 15.2.1 [Alexis Svinartchouk]
|
||||
* Fix source-selector image height [Lorenzo Alberto Maria Ambrosi]
|
||||
* Update rendition to v15.0.0 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Merge unsafe mode with new target selector [Lorenzo Alberto Maria Ambrosi]
|
||||
* Rework target selector modal [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.99
|
||||
## (2020-06-12)
|
||||
|
||||
* Update node-raspberrypi-usbboot to 0.2.8 [Alexis Svinartchouk]
|
||||
* Update electron to 9.0.3 [Alexis Svinartchouk]
|
||||
* Inline all svgs [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.98
|
||||
## (2020-06-10)
|
||||
|
||||
* Use between 2 and 256MiB for buffering depending on the number of drives [Alexis Svinartchouk]
|
||||
* Check that argument is an url or a regular file before opening [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to ^4.1.13 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.97
|
||||
## (2020-06-08)
|
||||
|
||||
* Update electron to v9.0.2 [Alexis Svinartchouk]
|
||||
* Fix flash from url on windows [Alexis Svinartchouk]
|
||||
* Avoid random access in http sources [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to ^4.1.8 [Alexis Svinartchouk]
|
||||
* Read image path from arguments, register `etcher://...` protocol [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to ^4.1.6 [Alexis Svinartchouk]
|
||||
* Fix sudo-prompt promisification [Alexis Svinartchouk]
|
||||
* Allow skipping notarization when building package (dev) [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.96
|
||||
## (2020-06-03)
|
||||
|
||||
* Fix ia32 builds for windows [Alexis Svinartchouk]
|
||||
* Remove writing speed from finish screen [Alexis Svinartchouk]
|
||||
* Add effective speed in flash results [Alexis Svinartchouk]
|
||||
* Update progress bar style [Alexis Svinartchouk]
|
||||
* Change font to SourceSansPro and fix hover color [Alexis Svinartchouk]
|
||||
* Update rendition to ^14.13.0 [Alexis Svinartchouk]
|
||||
* Remove unused styles [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.95
|
||||
## (2020-06-01)
|
||||
|
||||
* spectron: Make tests pass on Windows Docker containers [Juan Cruz Viotti]
|
||||
|
||||
# v1.5.94
|
||||
## (2020-05-27)
|
||||
|
||||
* Stop checking file extensions [Alexis Svinartchouk]
|
||||
* Fix flash from url (broken in 1.5.92) [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to ^4.1.4 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.93
|
||||
## (2020-05-25)
|
||||
|
||||
* Update electron-builder to v22.6.1 [Alexis Svinartchouk]
|
||||
* Strip out comments from generated code [Alexis Svinartchouk]
|
||||
* Update electron to v9.0.0 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.92
|
||||
## (2020-05-22)
|
||||
|
||||
* Use electron.app.getAppPath() instead of reading it from argv in catalina-sudo [Alexis Svinartchouk]
|
||||
* Disable asar packing on all platforms [Alexis Svinartchouk]
|
||||
* Remove unneeded fortawesome from main.scss [Alexis Svinartchouk]
|
||||
* Remove unneeded font formats [Alexis Svinartchouk]
|
||||
* Webpack everything, reduce package size [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.91
|
||||
## (2020-05-21)
|
||||
|
||||
* Minor fix - Init isSourceDrive param in correct place [Lorenzo Alberto Maria Ambrosi]
|
||||
* Fix undefined image from DriveCompatibilityWarning [Rob Evans]
|
||||
|
||||
# v1.5.90
|
||||
## (2020-05-20)
|
||||
|
||||
* Update leds behaviour [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.89
|
||||
## (2020-05-13)
|
||||
|
||||
* Fix drive selector modal padding [Alexis Svinartchouk]
|
||||
* Update all dependencies minor versions [Alexis Svinartchouk]
|
||||
* Update @types/node 12.12.24 -> 12.12.39 [Alexis Svinartchouk]
|
||||
* Update ts-loader 6 -> 7 [Alexis Svinartchouk]
|
||||
* Update sinon 8 -> 9 [Alexis Svinartchouk]
|
||||
* Update node-gyp 3 -> 6 [Alexis Svinartchouk]
|
||||
* Update lint-staged 9 -> 10 [Alexis Svinartchouk]
|
||||
* Update husky 3 -> 4 [Alexis Svinartchouk]
|
||||
* Remove no longer used html-loader dev dependency [Alexis Svinartchouk]
|
||||
* Update electron-notarize 0.1.1 -> 0.3.0 [Alexis Svinartchouk]
|
||||
* Remove no longer used chalk dev dependency [Alexis Svinartchouk]
|
||||
* Update @types/tmp 0.1.0 -> 0.2.0 [Alexis Svinartchouk]
|
||||
* Update @types/sinon 7 -> 9 [Alexis Svinartchouk]
|
||||
* Update @types/semver 6 -> 7 [Alexis Svinartchouk]
|
||||
* Update @types/mocha 5 -> 7 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.88
|
||||
## (2020-05-12)
|
||||
|
||||
* Update roboto-fontface 0.9.0 -> 0.10.0 [Alexis Svinartchouk]
|
||||
* Update rendition 12 -> 14, styled-system and styled-components 4 -> 5 [Alexis Svinartchouk]
|
||||
* Update electron-updater 4.0.6 -> 4.3.1 [Alexis Svinartchouk]
|
||||
* Update redux 3 -> 4 [Alexis Svinartchouk]
|
||||
* Update debug 3 -> 4 [Alexis Svinartchouk]
|
||||
* Update semver 5 -> 7 [Alexis Svinartchouk]
|
||||
* Update tmp 0.1.0 -> 0.2.1 [Alexis Svinartchouk]
|
||||
* Update uuid v3 -> v8 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.87
|
||||
## (2020-05-12)
|
||||
|
||||
* Update etcher-sdk to ^4.1.3 to fix issues with some bz2 files [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.86
|
||||
## (2020-05-06)
|
||||
|
||||
* Fix theme warnings [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.85
|
||||
## (2020-05-05)
|
||||
|
||||
* Prefer balena-etcher to etcher-bin on Arch Linux [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.84
|
||||
## (2020-05-04)
|
||||
|
||||
* Including Arch / Manjaro install instructions [Tom]
|
||||
* Fix notification icon path [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.83
|
||||
## (2020-04-30)
|
||||
|
||||
* Decompress images before flashing, remove trim setting, trim ext partitions [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.82
|
||||
## (2020-04-24)
|
||||
|
||||
* Allow http/https only for Flash from URL [Lorenzo Alberto Maria Ambrosi]
|
||||
* Add generic error's message [Lorenzo Alberto Maria Ambrosi]
|
||||
* Refactor buttons style [Lorenzo Alberto Maria Ambrosi]
|
||||
* Add flash from url workflow [Lorenzo Alberto Maria Ambrosi]
|
||||
* Add staging percentage for v1.5.81 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Trigger update for v1.5.81 [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.81
|
||||
## (2020-04-14)
|
||||
|
||||
* Add average speed in flash results [Lorenzo Alberto Maria Ambrosi]
|
||||
* docs: Update macOS drive recovery command [Wilson de Farias]
|
||||
* Update etcher-sdk to use direct IO [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.80
|
||||
## (2020-03-24)
|
||||
|
||||
* Use zoomFactor to scale contents in fullscreen mode [Lorenzo Alberto Maria Ambrosi]
|
||||
* Update electron to v7.1.14 [Alexis Svinartchouk]
|
||||
* Fix sass files path for lint-sass [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.79
|
||||
## (2020-02-20)
|
||||
|
||||
* Remove "Download the React DevTools for a better development experience" message [Alexis Svinartchouk]
|
||||
* Fix error when launching from terminal when installed via apt. [Alois Klink]
|
||||
|
||||
# v1.5.78
|
||||
## (2020-02-19)
|
||||
|
||||
* Update drivelist to 8.0.10 to fix parsing lsblk --pairs [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.77
|
||||
## (2020-02-17)
|
||||
|
||||
* Fix error message not being shown on write error [Alexis Svinartchouk]
|
||||
* The RGBLed module has been moved to a separate repository [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.76
|
||||
## (2020-02-05)
|
||||
|
||||
* Prefix temp permissions script name [Lorenzo Alberto Maria Ambrosi]
|
||||
* Fix image drop zone, remove react-dropzone dependency [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to ^2.0.17 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.75
|
||||
## (2020-02-05)
|
||||
|
||||
* Initialize leds object map [Omar López]
|
||||
|
||||
# v1.5.74
|
||||
## (2020-02-04)
|
||||
|
||||
* Etcher pro leds feature [Alexis Svinartchouk]
|
||||
* Compress deb package with bzip instead of xz [Alexis Svinartchouk]
|
||||
* Update electron to 7.1.11 [Alexis Svinartchouk]
|
||||
* Sort devices by device path on Linux [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.73
|
||||
## (2020-01-28)
|
||||
|
||||
* Update electron to v7.1.10 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.72
|
||||
## (2020-01-27)
|
||||
|
||||
* Remove no longer used angular svg-icon component [Alexis Svinartchouk]
|
||||
* Remove no longer used closestUnit angular filter [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.71
|
||||
## (2020-01-14)
|
||||
|
||||
* Update resin-corvus to 2.0.5 [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.70
|
||||
## (2019-12-13)
|
||||
|
||||
* Make header draggable again [Lorenzo Alberto Maria Ambrosi]
|
||||
* Refactor drive selector and confirm modal to React [Lorenzo Alberto Maria Ambrosi]
|
||||
* Rework lib/gui/app/styled-components to typescript [Alexis Svinartchouk]
|
||||
* Convert FlashAnother & FlashResults to typescript [Lorenzo Alberto Maria Ambrosi]
|
||||
* Use React instead of Angular for image selection [Lucian]
|
||||
* Convert the drive selection step to React [Thodoris Greasidis]
|
||||
* chore: move flash step to React [Stevche Radevski]
|
||||
* Use React instead of Angular for image selection [Lucian]
|
||||
|
||||
# v1.5.69
|
||||
## (2019-12-10)
|
||||
|
||||
* Don't add --no-sandbox when ELECTRON_RUN_AS_NODE true [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.68
|
||||
## (2019-12-08)
|
||||
|
||||
* Add version in settings modal [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.67
|
||||
## (2019-12-06)
|
||||
|
||||
* Fix elevation on macos in development [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.66
|
||||
## (2019-12-03)
|
||||
|
||||
* Update spectron to ^8 [Alexis Svinartchouk]
|
||||
* Update dependencies, get node-usb from npm [Alexis Svinartchouk]
|
||||
* Update nan to ^2.14 [Alexis Svinartchouk]
|
||||
* Use the same entrypoint for etcher and the child writer [Alexis Svinartchouk]
|
||||
* Require angular-mocks only when needed [Alexis Svinartchouk]
|
||||
* Remove no longer needed pkg dev dependency [Alexis Svinartchouk]
|
||||
* Update mocha, remove nock [Alexis Svinartchouk]
|
||||
* Remove no longer needed xml2js [Alexis Svinartchouk]
|
||||
* Remove node-pre-gyp patch that is no longer needed with electron 6 [Alexis Svinartchouk]
|
||||
* Update electron-mocha to ^8.1.2, remove acorn [Alexis Svinartchouk]
|
||||
* Update electron to 6.0.10 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.65
|
||||
## (2019-12-02)
|
||||
|
||||
* Convert settings modal to typescript [Lorenzo Alberto Maria Ambrosi]
|
||||
* Refactor settings page into modal [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.64
|
||||
## (2019-11-22)
|
||||
|
||||
* Use bash instead of sh for running the elevated process on Linux and Mac [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.63
|
||||
## (2019-11-08)
|
||||
|
||||
* Introduce an FAQ file [Dimitrios Lytras]
|
||||
|
||||
# v1.5.62
|
||||
## (2019-11-06)
|
||||
|
||||
* Update drivelist to 8.0.9 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.61
|
||||
## (2019-11-05)
|
||||
|
||||
* Notarize app on macOS [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.60
|
||||
## (2019-10-18)
|
||||
|
||||
* Upgrade ext2fs to 1.0.30 [Matthew McGinn]
|
||||
|
||||
# v1.5.59
|
||||
## (2019-10-14)
|
||||
|
||||
* Catch console log messages from SafeWebView [Roman Mazur]
|
||||
|
||||
# v1.5.58
|
||||
## (2019-10-10)
|
||||
|
||||
* Remove leftover GH-pages configuration file [Dimitrios Lytras]
|
||||
|
||||
# v1.5.57
|
||||
## (2019-09-16)
|
||||
|
||||
* Fix entrypoint when options are passed to electron [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.56
|
||||
## (2019-08-20)
|
||||
|
||||
* Fix windows portable download [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.55
|
||||
## (2019-08-19)
|
||||
|
||||
* Update etcher-sdk to ^2.0.13 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.54
|
||||
## (2019-08-07)
|
||||
|
||||
* Fix auto-updater check for updates [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.53
|
||||
## (2019-08-06)
|
||||
|
||||
* Allow typescript files [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.52
|
||||
## (2019-07-22)
|
||||
|
||||
* Don't use wmic's ProviderName if it's empty [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.51
|
||||
## (2019-06-28)
|
||||
|
||||
* Update sudo-prompt to ^9.0.0 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.50
|
||||
## (2019-06-13)
|
||||
|
||||
* Option for trimming ext partitions on raw images [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.49
|
||||
## (2019-06-13)
|
||||
|
||||
* Make window size configurable [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.48
|
||||
## (2019-06-13)
|
||||
|
||||
* Don't use sudo-prompt when already elevated [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.47
|
||||
## (2019-06-10)
|
||||
|
||||
* Rework drive-selector with react + rendition [Lorenzo Alberto Maria Ambrosi]
|
||||
* Use rendition theme property for step buttons [Lorenzo Alberto Maria Ambrosi]
|
||||
* Upgrade styled-system to v4.1.0 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Upgrade rendition to v8.7.2 [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.46
|
||||
## (2019-06-09)
|
||||
|
||||
* Update ext2fs to 1.0.29 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.45
|
||||
## (2019-06-04)
|
||||
|
||||
* Empty commit to trigger build [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.44
|
||||
## (2019-06-03)
|
||||
|
||||
* Fix elevation on windows when the path contains "&" or "'" [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.43
|
||||
## (2019-05-28)
|
||||
|
||||
* Revert "Include sass in webpack configs" [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.42
|
||||
## (2019-05-28)
|
||||
|
||||
* Include sass in webpack configs [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.41
|
||||
## (2019-05-27)
|
||||
|
||||
* waffle.io removal and adding a link to the license [Mateusz Hajder]
|
||||
|
||||
# v1.5.40
|
||||
## (2019-05-24)
|
||||
|
||||
* windows installer and portable version support both ia32 and x64 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.39
|
||||
## (2019-05-14)
|
||||
|
||||
* Add clean-shrinkwrap script to postshrinkwrap step [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.38
|
||||
## (2019-05-13)
|
||||
|
||||
* Add mention to usbboot compatibility [Carlo Maria Curinga]
|
||||
|
||||
# v1.5.37
|
||||
## (2019-05-13)
|
||||
|
||||
* Bump react dependency to v16.8.5 [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.36
|
||||
## (2019-05-13)
|
||||
|
||||
* Update etcher-sdk to ^2.0.9 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.35
|
||||
## (2019-05-10)
|
||||
|
||||
* Downgrade electron 4.1.5 -> 3.1.9 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.34
|
||||
## (2019-05-09)
|
||||
|
||||
* Use https url for fetching config, avoid redirection [Alexis Svinartchouk]
|
||||
* win32: fix running diskpart when the tmp file path contains spaces [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.33
|
||||
## (2019-04-30)
|
||||
|
||||
* Fix gzipped files verification percentage and dmg verification. [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.32
|
||||
## (2019-04-30)
|
||||
|
||||
* Export NPM_VERSION variable in Makefile [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.31
|
||||
## (2019-04-29)
|
||||
|
||||
* Update etcher-sdk to ^2.0.3 [Alexis Svinartchouk]
|
||||
* Update electron to 4.1.5 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.30
|
||||
## (2019-04-24)
|
||||
|
||||
* Don't show a dialog when the write fails. [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.29
|
||||
## (2019-04-19)
|
||||
|
||||
* Add support for auto-updating feature [Giovanni Garufi]
|
||||
|
||||
# v1.5.28
|
||||
## (2019-04-18)
|
||||
|
||||
* Update electron-builder to ^20.40.2 [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to ^2.0.1 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.27
|
||||
## (2019-04-16)
|
||||
|
||||
* (Windows): Fix reading images from network drives when the tmp dir has spaces [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.26
|
||||
## (2019-04-12)
|
||||
|
||||
* (Windows): Fix reading images from network drives containing non ascii characters [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.25
|
||||
## (2019-04-09)
|
||||
|
||||
* New parameter in webview for opt-out analytics [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.24
|
||||
## (2019-04-05)
|
||||
|
||||
* Update resin-corvus to ^2.0.3 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.23
|
||||
## (2019-04-03)
|
||||
|
||||
* Configure versionbot to publish repo metadata to github pages [Giovanni Garufi]
|
||||
|
||||
# v1.5.22
|
||||
## (2019-04-02)
|
||||
|
||||
* (Windows): Use full path to wmic as some systems don't have it in their PATH [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.21
|
||||
## (2019-04-02)
|
||||
|
||||
* Fix error when config.analytics was undefined [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.20
|
||||
## (2019-04-01)
|
||||
|
||||
* Don't try to flash when no device is selected [Alexis Svinartchouk]
|
||||
* Reformat changelog [Giovanni Garufi]
|
||||
* Avoid "Error: There is already a flash in progress" errors [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.19
|
||||
## (2019-03-28)
|
||||
|
||||
* Update resin-corvus to ^2.0.2 [Alexis Svinartchouk]
|
||||
* Better reporting of unhandled rejections to sentry [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.18
|
||||
## (2019-03-26)
|
||||
|
||||
* Update build scripts [Giovanni Garufi]
|
||||
|
||||
## v1.5.17 - 2019-03-25
|
||||
|
||||
### Misc
|
||||
|
||||
- Automatically publish github release from CI
|
||||
|
||||
## v1.5.16 - 2019-03-25
|
||||
|
||||
### Misc
|
||||
|
||||
- Add repo.yml
|
||||
|
||||
## v1.5.15 - 2019-03-20
|
||||
|
||||
### Misc
|
||||
|
||||
- Show the correct logo on usbboot devices on Ubuntu
|
||||
|
||||
## v1.5.14 - 2019-03-20
|
||||
|
||||
### Misc
|
||||
|
||||
- Update etcher-sdk to ^1.3.10
|
||||
|
||||
## v1.5.13 - 2019-03-18
|
||||
|
||||
### Misc
|
||||
|
||||
- Update build scripts
|
||||
|
||||
## v1.5.12 - 2019-03-15
|
||||
|
||||
### Misc
|
||||
|
||||
- Update build scripts
|
||||
|
||||
## v1.5.11 - 2019-03-12
|
||||
|
||||
### Misc
|
||||
|
||||
- Fixed broken Hombrew cask link for etcher
|
||||
- Remove no longer used travis and appveyor configs
|
||||
|
||||
## v1.5.10 - 2019-03-12
|
||||
|
||||
### Misc
|
||||
|
||||
- Update resin-scripts
|
||||
|
||||
## v1.5.9 - 2019-03-05
|
||||
|
||||
### Misc
|
||||
|
||||
- Update etcher-sdk to 1.3.0
|
||||
|
||||
## v1.5.8 - 2019-03-01
|
||||
|
||||
### Misc
|
||||
|
||||
- Update ext2fs to 1.0.27
|
||||
|
||||
## v1.5.7 - 2019-03-01
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update docs
|
||||
- Fix disappearing modal window
|
||||
|
||||
### Misc
|
||||
|
||||
- Fix blurred background image
|
||||
|
||||
## v1.5.6 - 2019-02-28
|
||||
|
||||
### Misc
|
||||
|
||||
- Target electron 3 runtime in babel options
|
||||
|
||||
## v1.5.5 - 2019-02-28
|
||||
|
||||
### Misc
|
||||
|
||||
- Don't pass undefined sockets to ipc.server.emit()
|
||||
- Fix error when event.dataTransfer.files is empty
|
||||
- Fix error message not showing when an unsupported image is selected
|
||||
- Avoid `Invalid percentage` exceptions
|
||||
- Update etcher-sdk to 1.1.0
|
||||
|
||||
## v1.5.4 - 2019-02-27
|
||||
|
||||
### Misc
|
||||
|
||||
- Add missing step for submodule cloning in README
|
||||
|
||||
## v1.5.3 - 2019-02-27
|
||||
|
||||
### Misc
|
||||
|
||||
- Throw error if no commit is annotated with a changelog entry
|
||||
|
||||
## v1.5.2 - 2019-02-26
|
||||
|
||||
- Enable versionist editVersion
|
||||
|
||||
## v1.5.1 - 2019-02-22
|
||||
|
||||
### Misc
|
||||
|
||||
- Removed lodash dependency in versionist.conf.js
|
||||
|
||||
## v1.5.0 - 2019-02-16
|
||||
|
||||
### Misc
|
||||
|
||||
- Reworked flashing logic with etcher-sdk
|
||||
- Add support for flashing Raspberry Pi CM3+
|
||||
- Upgrade to Electron v3.
|
||||
- Upgrade to NPM 6.7.0
|
||||
- Fix incorrect drives list on Linux
|
||||
- Changed “Drive Contains Image” to “Drive Mountpoint Contains Image”
|
||||
- Removed etcher-cli
|
||||
|
||||
## v1.4.9 - 2018-12-19
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix update notifier error popping up on v1.4.1->1.4.8
|
||||
|
||||
### Misc
|
||||
|
||||
- Added React component for the Flash Results button
|
||||
- Added React component for the Flash Another button
|
||||
- Restyle success screen and enlarge UI elements
|
||||
- Use https for fetching sub modules
|
||||
- Add `.wic` image extension as supported format
|
||||
|
||||
## v1.4.8 - 2018-11-23
|
||||
|
||||
### Features
|
||||
|
||||
- Added featured-project while flashing
|
||||
|
||||
### Fixes
|
||||
|
||||
- Moved back the write cancel button
|
||||
- Reject drives with null size (fixes pretty-bytes error)
|
||||
|
||||
## v1.4.7 - 2018-11-12
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix typo in contributing guidelines
|
||||
- Modify versionist.conf.js to match new internal commit guidelines
|
||||
|
||||
### Misc
|
||||
|
||||
- Rename etcher to balena-etcher
|
||||
- Convert Select Image button to Rendition
|
||||
|
||||
## v1.4.6 - 2018-10-28
|
||||
|
||||
### Fixes
|
||||
|
||||
- Provide a Buffer to xxhash.Stream
|
||||
- Fix 64 bit detection on arm
|
||||
- Fix incorrect file constraint path
|
||||
- Fix flash cancel button interaction
|
||||
|
||||
### Misc
|
||||
|
||||
- Add new balena.io logos
|
||||
- Use Resin CI scripts to build Etcher
|
||||
- Enable React lint rules
|
||||
- Convert Progress Button to Rendition
|
||||
|
||||
## v1.4.5 - 2018-10-11
|
||||
|
||||
### Features
|
||||
|
||||
- Center content independent to window resolution.
|
||||
- Add electron-native file-picker component.
|
||||
- Hide unsafe mode option toggle with an env var.
|
||||
- Use new design background color and drive step size ordering.
|
||||
- Add a convenience Storage class on top of localStorage.
|
||||
- Introduce env var to toggle autoselection of all drives.
|
||||
- Add font-awesome.
|
||||
- Add support for configuration files
|
||||
- Use GTK-3 darkTheme mode.
|
||||
- Add environment variable to toggle fullscreen.
|
||||
- Allow blacklisting of drives through and environment variable ETCHER_BLACKLISTED_DRIVES.
|
||||
- Show selected drives below drive selection step.
|
||||
- Add a button to cancel the flash process.
|
||||
- Download usbboot drivers installer when clicking a driverless usbboot device on Windows.
|
||||
- Allow disabling links and hiding help link with an env var.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Add "make webpack" to travis-ci build script
|
||||
- Makefile: Don't use tilde in rpm versions
|
||||
- Change Spectron port so not to overlap with other builds
|
||||
- Fix multi-writes analytics by reusing existing logic in multi-write events.
|
||||
- Load usbboot adapter on start on GNU/Linux if running as root.
|
||||
|
||||
### Misc
|
||||
|
||||
- Update drivelist to v6.4.2
|
||||
- Add instructions for installing and uninstalling on Solus.
|
||||
|
||||
## v1.4.4 - 2018-04-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't display status dots with a quantity of zero on success screen
|
||||
- Correct wording of flash status to use "successful" instead of "succeeded"
|
||||
- Keep single drive-image pairs with warnings selected
|
||||
|
||||
### Misc
|
||||
|
||||
- Improve notification messages
|
||||
|
||||
## v1.4.3 - 2018-04-19
|
||||
|
||||
### Fixes
|
||||
|
2
CODEOWNERS
Normal file
@@ -0,0 +1,2 @@
|
||||
* @thundron @zvin @jviotti
|
||||
/scripts @nazrhom
|
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.9 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
528
Makefile
@@ -2,27 +2,22 @@
|
||||
# Build configuration
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# A non-existing target to force rules to rebuild
|
||||
# See https://stackoverflow.com/a/816416
|
||||
.FORCE:
|
||||
RESIN_SCRIPTS ?= ./scripts/resin
|
||||
export NPM_VERSION ?= 6.14.5
|
||||
S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||
|
||||
# This directory will be completely deleted by the `clean` rule
|
||||
BUILD_DIRECTORY ?= dist
|
||||
|
||||
# See http://stackoverflow.com/a/20763842/1641422
|
||||
BUILD_DIRECTORY_PARENT = $(dir $(BUILD_DIRECTORY))
|
||||
ifeq ($(wildcard $(BUILD_DIRECTORY_PARENT).),)
|
||||
$(error $(BUILD_DIRECTORY_PARENT) does not exist)
|
||||
endif
|
||||
|
||||
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
|
||||
|
||||
# See https://github.com/electron/spectron/issues/127
|
||||
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
|
||||
$(BUILD_DIRECTORY):
|
||||
mkdir $@
|
||||
|
||||
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
|
||||
# See https://stackoverflow.com/a/13468229/1641422
|
||||
SHELL := /bin/bash
|
||||
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Operating system and architecture detection
|
||||
@@ -75,10 +70,10 @@ else
|
||||
endif
|
||||
|
||||
ifndef PLATFORM
|
||||
$(error We couldn't detect your host platform)
|
||||
$(error We could not detect your host platform)
|
||||
endif
|
||||
ifndef HOST_ARCH
|
||||
$(error We couldn't detect your host architecture)
|
||||
$(error We could not detect your host architecture)
|
||||
endif
|
||||
|
||||
# Default to host architecture. You can override by doing:
|
||||
@@ -87,289 +82,31 @@ endif
|
||||
#
|
||||
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:
|
||||
$(RESIN_SCRIPTS)/electron/install.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-s $(PLATFORM) \
|
||||
-m $(NPM_VERSION)
|
||||
|
||||
ELECTRON_VERSION = $(shell jq -r '.devDependencies["electron"]' package.json)
|
||||
NODE_VERSION = 6.1.0
|
||||
COMPANY_NAME = Resinio Ltd
|
||||
APPLICATION_NAME = $(shell jq -r '.displayName' package.json)
|
||||
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
|
||||
# ---------------------------------------------------------------------
|
||||
electron-test:
|
||||
$(RESIN_SCRIPTS)/electron/test.sh \
|
||||
-b $(shell pwd) \
|
||||
-s $(PLATFORM)
|
||||
|
||||
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
|
||||
tiffutil -cathidpicheck $^ -out $@
|
||||
|
||||
build/js/gui.js: .FORCE
|
||||
webpack
|
||||
|
||||
$(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 $<) \
|
||||
electron-build: assets/dmg/background.tiff | $(BUILD_TEMPORARY_DIRECTORY)
|
||||
$(RESIN_SCRIPTS)/electron/build.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-b $(APPLICATION_NAME_ELECTRON) \
|
||||
-i assets/icon.png \
|
||||
-o $@
|
||||
|
||||
$(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
|
||||
-s $(PLATFORM) \
|
||||
-v production \
|
||||
-n $(BUILD_TEMPORARY_DIRECTORY)/npm
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Phony targets
|
||||
@@ -379,229 +116,36 @@ TARGETS = \
|
||||
help \
|
||||
info \
|
||||
lint \
|
||||
lint-js \
|
||||
lint-sass \
|
||||
lint-cpp \
|
||||
lint-html \
|
||||
lint-spell \
|
||||
test-spectron \
|
||||
test-gui \
|
||||
test-sdk \
|
||||
test-cli \
|
||||
test \
|
||||
sanity-checks \
|
||||
clean \
|
||||
distclean \
|
||||
changelog \
|
||||
webpack \
|
||||
package-electron \
|
||||
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)
|
||||
electron-develop \
|
||||
electron-test \
|
||||
electron-build
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
|
||||
cli-develop:
|
||||
./scripts/build/dependencies-npm.sh \
|
||||
-r "$(TARGET_ARCH)" \
|
||||
-v "$(NODE_VERSION)" \
|
||||
-t node \
|
||||
-s "$(PLATFORM)"
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
electron-develop:
|
||||
./scripts/build/dependencies-npm.sh \
|
||||
-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
|
||||
test:
|
||||
npm run test
|
||||
|
||||
help:
|
||||
@echo "Available targets: $(TARGETS)"
|
||||
|
||||
info:
|
||||
@echo "Application version : $(APPLICATION_VERSION)"
|
||||
@echo "Release type : $(RELEASE_TYPE)"
|
||||
@echo "Platform : $(PLATFORM)"
|
||||
@echo "Host arch : $(HOST_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:
|
||||
rm -rf $(BUILD_DIRECTORY)
|
||||
|
||||
distclean: clean
|
||||
rm -rf node_modules
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
rm -rf generated
|
||||
rm -rf $(BUILD_TEMPORARY_DIRECTORY)
|
||||
|
||||
.DEFAULT_GOAL = help
|
||||
|
139
README.md
@@ -1,40 +1,32 @@
|
||||
Etcher
|
||||
======
|
||||
# Etcher
|
||||
|
||||
> Flash OS images to SD cards & USB drives, safely and easily.
|
||||
|
||||
Etcher is a powerful OS image flasher built with web technologies to ensure
|
||||
flashing an SDCard or USB drive is a pleasant and safe experience. It protects
|
||||
you from accidentally writing to your hard-drives, ensures every byte of data
|
||||
was written correctly and much more.
|
||||
was written correctly and much more. It can also flash directly Raspberry Pi devices that support the usbboot protocol
|
||||
|
||||
[](https://etcher.io)
|
||||

|
||||
[](https://travis-ci.org/resin-io/etcher/branches)
|
||||
[](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)
|
||||
[](https://balena.io/etcher)
|
||||
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||
[](https://david-dm.org/balena-io/etcher)
|
||||
[](https://forums.balena.io/c/etcher)
|
||||
|
||||
***
|
||||
|
||||
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones] | [**CLI**][CLI]
|
||||
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones]
|
||||
|
||||

|
||||
|
||||
Supported Operating Systems
|
||||
---------------------------
|
||||
## Supported Operating Systems
|
||||
|
||||
- Linux (most distros)
|
||||
- macOS 10.9 and later
|
||||
- macOS 10.10 (Yosemite) and later
|
||||
- Microsoft Windows 7 and later
|
||||
|
||||
Note that Etcher will run on any platform officially supported by
|
||||
[Electron][electron]. Read more in their
|
||||
[documentation][electron-supported-platforms].
|
||||
|
||||
Installers
|
||||
----------
|
||||
## Installers
|
||||
|
||||
Refer to the [downloads page][etcher] for the latest pre-made
|
||||
installers for all supported operating systems.
|
||||
@@ -43,64 +35,106 @@ installers for all supported operating systems.
|
||||
|
||||
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
|
||||
```sh
|
||||
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
|
||||
```
|
||||
|
||||
2. Trust Bintray.com's GPG key:
|
||||
|
||||
```sh
|
||||
sudo apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 379CE192D401AB61
|
||||
sudo apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys 379CE192D401AB61
|
||||
```
|
||||
|
||||
3. Update and install:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install etcher-electron
|
||||
sudo apt-get install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo apt-get remove etcher-electron
|
||||
sudo rm /etc/apt/sources.list.d/etcher.list
|
||||
sudo apt-get remove balena-etcher-electron
|
||||
sudo rm /etc/apt/sources.list.d/balena-etcher.list
|
||||
sudo apt-get update
|
||||
```
|
||||
|
||||
##### OpenSUSE LEAP & Tumbleweed install
|
||||
|
||||
```sh
|
||||
sudo zypper ar https://balena.io/etcher/static/etcher-rpm.repo
|
||||
sudo zypper ref
|
||||
sudo zypper in balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo zypper rm balena-etcher-electron
|
||||
```
|
||||
|
||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
sudo wget https://bintray.com/resin-io/redhat/rpm -O /etc/yum.repos.d/bintray-resin-io-redhat.repo
|
||||
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo yum install -y etcher-electron
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf install -y etcher-electron
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```
|
||||
sudo yum remove -y etcher-electron
|
||||
sudo rm /etc/yum.repos.d/bintray-resin-io-redhat.repo
|
||||
```sh
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo yum clean all
|
||||
sudo yum makecache fast
|
||||
```
|
||||
or
|
||||
```
|
||||
sudo dnf remove -y etcher-electron
|
||||
sudo rm /etc/yum.repos.d/bintray-resin-io-redhat.repo
|
||||
```sh
|
||||
sudo dnf remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
```
|
||||
|
||||
#### Solus (GNU/Linux x64)
|
||||
|
||||
```sh
|
||||
sudo eopkg it etcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo eopkg rm etcher
|
||||
```
|
||||
|
||||
#### Arch Linux / Manjaro (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 Cask (macOS)
|
||||
|
||||
Note that the Etcher Cask has to be updated manually to point to new versions,
|
||||
@@ -108,16 +142,16 @@ so it might not refer to the latest version immediately after an Etcher
|
||||
release.
|
||||
|
||||
```sh
|
||||
brew cask install etcher
|
||||
brew cask install balenaetcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
brew cask uninstall etcher
|
||||
brew cask uninstall balenaetcher
|
||||
```
|
||||
|
||||
### Chocolatey (Windows)
|
||||
#### Chocolatey (Windows)
|
||||
|
||||
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
|
||||
is kept up to date automatically.
|
||||
@@ -126,25 +160,28 @@ is kept up to date automatically.
|
||||
choco install etcher
|
||||
```
|
||||
|
||||
Support
|
||||
-------
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
choco uninstall etcher
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
If you're having any problem, please [raise an issue][newissue] on GitHub and
|
||||
the resin.io team will be happy to help.
|
||||
the balena.io team will be happy to help.
|
||||
|
||||
License
|
||||
-------
|
||||
## License
|
||||
|
||||
Etcher is free software, and may be redistributed under the terms specified in
|
||||
the [license].
|
||||
|
||||
[etcher]: https://etcher.io
|
||||
[electron]: http://electron.atom.io
|
||||
[electron-supported-platforms]: http://electron.atom.io/docs/tutorial/supported-platforms/
|
||||
[SUPPORT]: https://github.com/resin-io/etcher/blob/master/SUPPORT.md
|
||||
[CONTRIBUTING]: https://github.com/resin-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[CLI]: https://github.com/resin-io/etcher/blob/master/docs/CLI.md
|
||||
[USER-DOCUMENTATION]: https://github.com/resin-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[milestones]: https://github.com/resin-io/etcher/milestones
|
||||
[newissue]: https://github.com/resin-io/etcher/issues/new
|
||||
[license]: https://github.com/resin-io/etcher/blob/master/LICENSE
|
||||
[etcher]: https://balena.io/etcher
|
||||
[electron]: https://electronjs.org/
|
||||
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||
[SUPPORT]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
||||
[CONTRIBUTING]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[USER-DOCUMENTATION]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[milestones]: https://github.com/balena-io/etcher/milestones
|
||||
[newissue]: https://github.com/balena-io/etcher/issues/new
|
||||
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE
|
||||
|
10
SUPPORT.md
@@ -8,7 +8,7 @@ Forums
|
||||
------
|
||||
|
||||
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
|
||||
a look at the existing threads before opening a new one!
|
||||
|
||||
@@ -20,7 +20,7 @@ support:
|
||||
- The operating system you're running Etcher in.
|
||||
|
||||
- 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
|
||||
------
|
||||
@@ -29,6 +29,6 @@ If you encounter an issue or have a suggestion, head on over to Etcher's [issue
|
||||
tracker][issues] and if there isn't a ticket covering it, [create
|
||||
one][new-issue].
|
||||
|
||||
[discourse]: https://forums.resin.io/c/etcher
|
||||
[issues]: https://github.com/resin-io/etcher/issues
|
||||
[new-issue]: https://github.com/resin-io/etcher/issues/new
|
||||
[discourse]: https://forums.balena.io/c/etcher
|
||||
[issues]: https://github.com/balena-io/etcher/issues
|
||||
[new-issue]: https://github.com/balena-io/etcher/issues/new
|
||||
|
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 |
26
beforeBuild.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use strict'
|
||||
|
||||
const cp = require('child_process');
|
||||
const rimraf = require('rimraf');
|
||||
const process = require('process');
|
||||
|
||||
// Rebuild native modules for ia32 and run webpack again for the ia32 part of windows packages
|
||||
exports.default = function(context) {
|
||||
if (context.platform.name === 'windows') {
|
||||
cp.execFileSync(
|
||||
'bash',
|
||||
['./node_modules/.bin/electron-rebuild', '--types', 'dev', '--arch', context.arch],
|
||||
);
|
||||
rimraf.sync('generated');
|
||||
cp.execFileSync(
|
||||
'bash',
|
||||
['./node_modules/.bin/webpack'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_target_arch: context.arch,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@@ -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]
|
||||
- [NodeJS][nodejs]
|
||||
- [AngularJS][angularjs]
|
||||
- [Redux][redux]
|
||||
- [ImmutableJS][immutablejs]
|
||||
- [Bootstrap][bootstrap]
|
||||
- [Sass][sass]
|
||||
- [Flexbox Grid][flexbox-grid]
|
||||
- [Mocha][mocha]
|
||||
- [JSDoc][jsdoc]
|
||||
|
||||
@@ -40,62 +37,18 @@ to submit their work or bug reports.
|
||||
|
||||
These are the main Etcher components, in a nutshell:
|
||||
|
||||
- [Etcher Image Write][etcher-image-write]
|
||||
|
||||
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)
|
||||
- [Drivelist](https://github.com/balena-io-modules/drivelist)
|
||||
|
||||
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
|
||||
a drive is removable or not, to prevent users from trying to write an image to
|
||||
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
|
||||
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
|
||||
-------
|
||||
|
||||
@@ -106,17 +59,12 @@ since fresh eyes could help unveil things that we take for granted, but should
|
||||
be documented instead!
|
||||
|
||||
[lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328
|
||||
[etcher-image-write]: https://github.com/resin-io-modules/etcher-image-write
|
||||
[exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/shared/exit-codes.js
|
||||
[cli-dir]: https://github.com/resin-io/etcher/tree/master/lib/cli
|
||||
[gui-dir]: https://github.com/resin-io/etcher/tree/master/lib/gui
|
||||
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/shared/exit-codes.js
|
||||
[gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui
|
||||
[electron]: http://electron.atom.io
|
||||
[nodejs]: https://nodejs.org
|
||||
[angularjs]: https://angularjs.org
|
||||
[redux]: http://redux.js.org
|
||||
[immutablejs]: http://facebook.github.io/immutable-js/
|
||||
[bootstrap]: http://getbootstrap.com
|
||||
[sass]: http://sass-lang.com
|
||||
[flexbox-grid]: http://flexboxgrid.com
|
||||
[mocha]: http://mochajs.org
|
||||
[jsdoc]: http://usejsdoc.org
|
||||
|
@@ -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
|
||||
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
|
||||
-------
|
||||
|
||||
@@ -122,8 +117,8 @@ A commit can include multiple instances of this tag.
|
||||
Examples:
|
||||
|
||||
```
|
||||
Closes: https://github.com/resin-io/etcher/issues/XXX
|
||||
Fixes: https://github.com/resin-io/etcher/issues/XXX
|
||||
Closes: https://github.com/balena-io/etcher/issues/XXX
|
||||
Fixes: https://github.com/balena-io/etcher/issues/XXX
|
||||
```
|
||||
|
||||
### `Change-Type: <type>`
|
||||
@@ -198,7 +193,7 @@ first non compressed extension.
|
||||
|
||||
Change-Type: patch
|
||||
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
|
||||
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
|
||||
Fixes: https://github.com/resin-io/etcher/issues/531
|
||||
Link: https://github.com/balena-io-modules/etcher-image-write/blob/master/CHANGELOG.md#502---2016-06-27
|
||||
Fixes: https://github.com/balena-io/etcher/issues/531
|
||||
```
|
||||
|
||||
***
|
||||
@@ -243,7 +238,7 @@ re-used by other services.
|
||||
|
||||
Change-Type: minor
|
||||
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
|
||||
|
@@ -17,10 +17,11 @@ Developing
|
||||
|
||||
#### Common
|
||||
|
||||
- [NodeJS](https://nodejs.org) (at least v6)
|
||||
- [NodeJS](https://nodejs.org) (at least v6.11)
|
||||
- [Python 2.7](https://www.python.org)
|
||||
- [jq](https://stedolan.github.io/jq/)
|
||||
- [curl](https://curl.haxx.se/)
|
||||
- [npm](https://www.npmjs.com/) (version 6.7)
|
||||
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
@@ -51,10 +52,12 @@ The following MinGW packages are required:
|
||||
- `msys-bash`
|
||||
- `msys-coreutils`
|
||||
|
||||
#### OS X
|
||||
#### macOS
|
||||
|
||||
- [XCode](https://developer.apple.com/xcode/) or [XCode Command Line Tools],
|
||||
which can be installed by running `xcode-select --install`.
|
||||
- [Xcode](https://developer.apple.com/xcode/)
|
||||
|
||||
It's not enough to have [Xcode Command Line Tools] installed. Xcode must be installed
|
||||
as well.
|
||||
|
||||
#### Linux
|
||||
|
||||
@@ -63,7 +66,7 @@ which can be installed by running `xcode-select --install`.
|
||||
### Cloning the project
|
||||
|
||||
```sh
|
||||
git clone https://github.com/resin-io/etcher
|
||||
git clone --recursive https://github.com/balena-io/etcher
|
||||
cd etcher
|
||||
```
|
||||
|
||||
@@ -93,12 +96,6 @@ make webpack
|
||||
npm start
|
||||
```
|
||||
|
||||
#### CLI
|
||||
|
||||
```sh
|
||||
node bin/etcher
|
||||
```
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
@@ -204,7 +201,7 @@ Before your pull request can be merged, the following conditions must hold:
|
||||
|
||||
- The linter doesn't throw any warning.
|
||||
|
||||
- All the tests passes.
|
||||
- All the tests pass.
|
||||
|
||||
- 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!
|
||||
|
||||
[ARCHITECTURE]: https://github.com/resin-io/etcher/blob/master/docs/ARCHITECTURE.md
|
||||
[COMMIT-GUIDELINES]: https://github.com/resin-io/etcher/blob/master/docs/COMMIT-GUIDELINES.md
|
||||
[ARCHITECTURE]: https://github.com/balena-io/etcher/blob/master/docs/ARCHITECTURE.md
|
||||
[COMMIT-GUIDELINES]: https://github.com/balena-io/etcher/blob/master/docs/COMMIT-GUIDELINES.md
|
||||
[EditorConfig]: http://editorconfig.org
|
||||
[shrinkwrap]: https://docs.npmjs.com/cli/shrinkwrap
|
||||
[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)
|
||||
- [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
|
||||
|
||||
#### Testing
|
||||
@@ -27,9 +27,10 @@ Releasing
|
||||
|
||||
#### Publishing
|
||||
|
||||
- [Publish release draft on GitHub](https://github.com/resin-io/etcher/releases)
|
||||
- [Post release note to forums](https://forums.resin.io/c/etcher)
|
||||
- [Update the website](https://github.com/resin-io/etcher-homepage)
|
||||
- [Publish release draft on GitHub](https://github.com/balena-io/etcher/releases)
|
||||
- [Post release note to forums](https://forums.balena.io/c/etcher)
|
||||
- [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
|
||||
- If regressions arise; pull the release, and release a patched version, else:
|
||||
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
|
||||
@@ -39,39 +40,6 @@ Releasing
|
||||
- Write a blog post about it, and / or
|
||||
- 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
|
||||
|
||||
**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"
|
||||
# Build AppImages
|
||||
./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
|
||||
|
||||
@@ -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"
|
||||
# Build AppImages
|
||||
./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
|
||||
@@ -124,13 +88,9 @@ export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_NAME` to generate signed binaries on Mac OS.
|
||||
|
||||
**NOTE:** The CLI is not code-signed for either at this time.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the CLI
|
||||
make RELEASE_TYPE=production cli-installer-tar-gz
|
||||
# Build the zip
|
||||
make RELEASE_TYPE=production electron-installer-app-zip
|
||||
# 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.
|
||||
|
||||
**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`.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the CLI
|
||||
make RELEASE_TYPE=production cli-installer-zip
|
||||
# Build the Portable version
|
||||
make RELEASE_TYPE=production electron-installer-portable
|
||||
# Build the Installer
|
||||
@@ -165,10 +122,10 @@ export BINTRAY_API_KEY="youruserapikey"
|
||||
```
|
||||
|
||||
```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 "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 "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 "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 "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 "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 "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 "x86" -f "dist/etcher-electron-1.2.1.i686.rpm"
|
||||
```
|
||||
|
||||
### Uploading binaries to Amazon S3
|
||||
@@ -178,7 +135,7 @@ export S3_KEY="..."
|
||||
```
|
||||
|
||||
```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
|
||||
@@ -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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
clicking on the certificate file.
|
||||
@@ -62,7 +62,7 @@ packaging for OS X.
|
||||
|
||||
### 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.
|
||||
|
||||
2. Place the certificate in the root of the Etcher repository naming it
|
||||
@@ -118,7 +118,7 @@ Publishing to S3
|
||||
- [AWS CLI][aws-cli]
|
||||
|
||||
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
|
||||
_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
|
||||
[`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.
|
||||
|
||||
Publishing to Homebrew Cask
|
||||
@@ -143,12 +143,12 @@ Publishing to Homebrew Cask
|
||||
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.
|
||||
|
||||
[aws-cli]: https://aws.amazon.com/cli
|
||||
[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
|
||||
[resin-forum-etcher]: https://forums.resin.io/c/etcher
|
||||
[github-releases]: https://github.com/resin-io/etcher/releases
|
||||
[balena-forum-etcher]: https://forums.balena.io/c/etcher
|
||||
[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
|
||||
-----------------------------------------------
|
||||
|
||||
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
|
||||
the application.
|
||||
|
||||
@@ -130,21 +130,6 @@ run Etcher on a GNU/Linux system.
|
||||
|
||||
- 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
|
||||
------------------------
|
||||
|
||||
@@ -181,7 +166,7 @@ Run the following command in `Terminal.app`, replacing `N` by the corresponding
|
||||
disk number, which you can find by running `diskutil list`:
|
||||
|
||||
```sh
|
||||
diskutil eraseDisk free UNTITLED /dev/diskN
|
||||
diskutil eraseDisk FAT32 UNTITLED MBRFormat /dev/diskN
|
||||
```
|
||||
|
||||
### GNU/Linux
|
||||
@@ -206,20 +191,16 @@ Running in older macOS versions
|
||||
-------------------------------
|
||||
|
||||
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
|
||||
platforms.
|
||||
|
||||
[resin.io]: https://resin.io
|
||||
[balena.io]: https://balena.io
|
||||
[appimage]: http://appimage.org
|
||||
[xwayland]: https://wayland.freedesktop.org/xserver.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
|
||||
[electron]: http://electron.atom.io
|
||||
[electron-supported-platforms]: https://github.com/electron/electron/blob/master/docs/tutorial/supported-platforms.md
|
||||
[etcher-cli]: https://github.com/resin-io/etcher/blob/master/docs/CLI.md
|
||||
[publishing]: https://github.com/resin-io/etcher/blob/master/docs/PUBLISHING.md
|
||||
[electron]: https://electronjs.org/
|
||||
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||
[publishing]: https://github.com/balena-io/etcher/blob/master/docs/PUBLISHING.md
|
||||
[windows-usb-tool]: https://www.microsoft.com/en-us/download/windows-usb-dvd-download-tool
|
||||
[rufus]: https://rufus.akeo.ie
|
||||
[unetbootin]: https://unetbootin.github.io
|
||||
|
@@ -1 +0,0 @@
|
||||
theme: jekyll-theme-minimal
|
@@ -1,20 +1,21 @@
|
||||
appId: io.resin.etcher
|
||||
copyright: Copyright 2016-2018 Resinio Ltd
|
||||
productName: Etcher
|
||||
npmRebuild: false
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2020 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: false
|
||||
publish: null
|
||||
beforeBuild: "./beforeBuild.js"
|
||||
afterPack: "./afterPack.js"
|
||||
asar: false
|
||||
files:
|
||||
- lib
|
||||
- "!lib/gui/app"
|
||||
- lib/gui/app/index.html
|
||||
- generated
|
||||
- build/**/*.node
|
||||
- assets/icon.png
|
||||
- node_modules/**/*
|
||||
- lib/shared/catalina-sudo/sudo-askpass.osascript.js
|
||||
mac:
|
||||
icon: assets/icon.icns
|
||||
category: public.app-category.developer-tools
|
||||
hardenedRuntime: true
|
||||
entitlements: "entitlements.mac.plist"
|
||||
entitlementsInherit: "entitlements.mac.plist"
|
||||
dmg:
|
||||
background: assets/dmg/background.tiff
|
||||
icon: assets/icon.icns
|
||||
@@ -38,15 +39,15 @@ nsis:
|
||||
uninstallerIcon: assets/icon.ico
|
||||
deleteAppDataOnUninstall: true
|
||||
license: LICENSE
|
||||
artifactName: "${productName}-Setup-${version}-${env.TARGET_ARCH}.${ext}"
|
||||
artifactName: "${productName}-Setup-${version}.${ext}"
|
||||
portable:
|
||||
artifactName: "${productName}-Portable-${version}-${env.TARGET_ARCH}.${ext}"
|
||||
artifactName: "${productName}-Portable-${version}.${ext}"
|
||||
requestExecutionLevel: user
|
||||
linux:
|
||||
category: Utility
|
||||
packageCategory: utils
|
||||
executableName: 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.
|
||||
executableName: balena-etcher-electron
|
||||
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
|
||||
deb:
|
||||
priority: optional
|
||||
@@ -63,16 +64,17 @@ deb:
|
||||
- libexpat1
|
||||
- libfontconfig1
|
||||
- libfreetype6
|
||||
- libgbm1
|
||||
- libgcc1
|
||||
- libgconf-2-4
|
||||
- libgdk-pixbuf2.0-0
|
||||
- libglib2.0-0
|
||||
- libgtk2.0-0
|
||||
- libgtk-3-0
|
||||
- liblzma5
|
||||
- libnotify4
|
||||
- libnspr4
|
||||
- libnss3
|
||||
- libpango1.0-0
|
||||
- libpango1.0-0 | libpango-1.0-0
|
||||
- libstdc++6
|
||||
- libx11-6
|
||||
- libxcomposite1
|
||||
@@ -88,5 +90,8 @@ deb:
|
||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||
rpm:
|
||||
depends:
|
||||
- lsb
|
||||
- libXScrnSaver
|
||||
- util-linux
|
||||
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,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)
|
||||
})
|
372
lib/gui/app/app.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* 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,
|
||||
isDriveValid,
|
||||
isSourceDrive,
|
||||
} from '../../shared/drive-constraints';
|
||||
import * as EXIT_CODES from '../../shared/exit-codes';
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as availableDrives from './models/available-drives';
|
||||
import * as flashState from './models/flash-state';
|
||||
import { init as ledsInit } from './models/leds';
|
||||
import { deselectImage, getImage, selectDrive } 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';
|
||||
|
||||
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.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
|
||||
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);
|
||||
if (
|
||||
(await settings.get('autoSelectAllDrives')) &&
|
||||
drive instanceof sdk.sourceDestination.BlockDevice &&
|
||||
// @ts-ignore BlockDevice.drive is private
|
||||
isDriveValid(drive.drive, getImage())
|
||||
) {
|
||||
selectDrive(drive.device);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
await ledsInit();
|
||||
ReactDOM.render(
|
||||
React.createElement(MainPage),
|
||||
document.getElementById('main'),
|
||||
// callback to set the correct zoomFactor for webviews as well
|
||||
async () => {
|
||||
const fullscreen = await settings.get('fullscreen');
|
||||
const width = fullscreen ? window.screen.width : window.outerWidth;
|
||||
try {
|
||||
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
@@ -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
|
534
lib/gui/app/components/drive-selector/drive-selector.tsx
Normal file
@@ -0,0 +1,534 @@
|
||||
/*
|
||||
* 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 DriveSVGIcon from '../../../assets/tgt.svg';
|
||||
import { SourceMetadata } from '../source-selector/source-selector';
|
||||
|
||||
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'> {
|
||||
multipleSelection: boolean;
|
||||
showWarnings?: boolean;
|
||||
cancel: () => void;
|
||||
done: (drives: DrivelistDrive[]) => void;
|
||||
titleLabel: string;
|
||||
emptyListLabel: string;
|
||||
selectedList?: DrivelistDrive[];
|
||||
updateSelectedList?: () => DrivelistDrive[];
|
||||
}
|
||||
|
||||
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>>;
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = 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}>{description}</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)
|
||||
);
|
||||
}
|
||||
|
||||
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 recommendedDriveSize =
|
||||
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
||||
return warning.unrecommendedDriveSize({ recommendedDriveSize }, drive);
|
||||
}
|
||||
}
|
||||
|
||||
private renderStatuses(drive: DrivelistDrive) {
|
||||
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
this.state.image,
|
||||
).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 } });
|
||||
}
|
||||
}
|
||||
|
||||
private deselectingAll(rows: DrivelistDrive[]) {
|
||||
return (
|
||||
rows.length > 0 &&
|
||||
rows.length === this.state.selectedList.length &&
|
||||
this.state.selectedList.every(
|
||||
(d) => rows.findIndex((r) => d.device === r.device) > -1,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
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%"
|
||||
>
|
||||
<DriveSVGIcon width="40px" height="90px" />
|
||||
<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 (this.deselectingAll(newSelection)) {
|
||||
newSelection = [];
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newSelection,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newSelection.slice(newSelection.length - 1),
|
||||
});
|
||||
}}
|
||||
onRowClick={(row: Drive) => {
|
||||
if (
|
||||
!isDrivelistDrive(row) ||
|
||||
this.driveShouldBeDisabled(row, image)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
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;
|
118
lib/gui/app/components/finish/finish.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 { 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();
|
||||
}
|
||||
|
||||
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
const [webviewShowing, setWebviewShowing] = React.useState(false);
|
||||
const flashResults = flashState.getFlashResults();
|
||||
let errors: FlashError[] = flashResults.results?.errors;
|
||||
if (errors === undefined) {
|
||||
errors = (store.getState().toJS().failedDevicePaths || []).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.getImageName()}
|
||||
results={results}
|
||||
skip={skip}
|
||||
errors={errors}
|
||||
mb="32px"
|
||||
goToMain={goToMain}
|
||||
/>
|
||||
|
||||
<FlashAnother
|
||||
onClick={() => {
|
||||
restart(goToMain);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<SafeWebview
|
||||
src="https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true"
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,15 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.modal-warning-modal .modal-content {
|
||||
width: 350px;
|
||||
import * as React from 'react';
|
||||
|
||||
import { BaseButton } from '../../styled-components';
|
||||
|
||||
export interface FlashAnotherProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
.modal-warning-modal .modal-title .glyphicon {
|
||||
color: $palette-theme-danger-background;
|
||||
}
|
||||
|
||||
.modal-warning-modal .modal-body {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||
return (
|
||||
<BaseButton primary onClick={props.onClick}>
|
||||
Flash another
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
235
lib/gui/app/components/flash-results/flash-results.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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 * as _ from 'lodash';
|
||||
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 { 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-cell'] {
|
||||
&:first-child {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DoneIcon = (props: {
|
||||
skipped: boolean;
|
||||
allFailed: boolean;
|
||||
someFailed: boolean;
|
||||
}) => {
|
||||
const { allFailed, someFailed } = props;
|
||||
const someOrAllFailed = allFailed || someFailed;
|
||||
const svgProps = {
|
||||
width: '24px',
|
||||
fill: someOrAllFailed ? '#c6c8c9' : '#1ac135',
|
||||
style: {
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
marginTop: '-25px',
|
||||
marginLeft: '13px',
|
||||
zIndex: 1,
|
||||
color: someOrAllFailed ? '#c6c8c9' : '#1ac135',
|
||||
},
|
||||
};
|
||||
return 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;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function FlashResults({
|
||||
goToMain,
|
||||
image = '',
|
||||
errors,
|
||||
results,
|
||||
skip,
|
||||
...props
|
||||
}: {
|
||||
goToMain: () => void;
|
||||
image?: string;
|
||||
errors: FlashError[];
|
||||
skip: boolean;
|
||||
results: {
|
||||
bytesWritten: number;
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
devices: { failed: number; successful: number };
|
||||
};
|
||||
} & FlexProps) {
|
||||
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||
const allFailed = results.devices.successful === 0;
|
||||
const effectiveSpeed = _.round(
|
||||
bytesToMegabytes(
|
||||
results.sourceMetadata.size /
|
||||
(results.bytesWritten / results.averageFlashingSpeed),
|
||||
),
|
||||
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}
|
||||
someFailed={results.devices.failed !== 0}
|
||||
/>
|
||||
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize={24} color="#fff" mb="17px">
|
||||
Flash Complete!
|
||||
</Txt>
|
||||
{skip ? <Flex color="#7e8085">Validation has been skipped</Flex> : null}
|
||||
</Flex>
|
||||
<Flex flexDirection="column" color="#7e8085">
|
||||
{Object.entries(results.devices).map(([type, quantity]) => {
|
||||
const failedTargets = type === 'failed';
|
||||
return quantity ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg
|
||||
width="14px"
|
||||
fill={type === 'failed' ? '#ff4444' : '#1ac135'}
|
||||
color={failedTargets ? '#ff4444' : '#1ac135'}
|
||||
/>
|
||||
<Txt ml="10px" color="#fff">
|
||||
{quantity}
|
||||
</Txt>
|
||||
<Txt
|
||||
ml="10px"
|
||||
tooltip={failedTargets ? formattedErrors(errors) : undefined}
|
||||
>
|
||||
{progress[type](quantity)}
|
||||
</Txt>
|
||||
{failedTargets && (
|
||||
<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();
|
||||
selection
|
||||
.getSelectedDrives()
|
||||
.filter((drive) =>
|
||||
errors.every((error) => error.device !== drive.device),
|
||||
)
|
||||
.forEach((drive) => selection.deselectDrive(drive.device));
|
||||
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: '@'
|
||||
}
|
||||
}
|
||||
}
|
135
lib/gui/app/components/progress-button/progress-button.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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, FlashState } from '../../modules/progress-status';
|
||||
import { StepButton } from '../../styled-components';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
> div {
|
||||
width: 220px;
|
||||
height: 12px;
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
transition-duration: 0s;
|
||||
> div {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
width: 220px;
|
||||
height: 12px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 14px;
|
||||
font-size: 16px;
|
||||
line-height: 48px;
|
||||
|
||||
background: #2f3033;
|
||||
`;
|
||||
|
||||
interface ProgressButtonProps {
|
||||
type: FlashState['type'];
|
||||
active: boolean;
|
||||
percentage: number;
|
||||
position: number;
|
||||
disabled: boolean;
|
||||
cancel: (type: string) => void;
|
||||
callback: () => void;
|
||||
warning?: boolean;
|
||||
}
|
||||
|
||||
const colors = {
|
||||
decompressing: '#00aeef',
|
||||
flashing: '#da60ff',
|
||||
verifying: '#1ac135',
|
||||
downloading: '#00aeef',
|
||||
default: '#00aeef',
|
||||
} 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 type = this.props.type || 'default';
|
||||
const percentage = this.props.percentage;
|
||||
const warning = this.props.warning;
|
||||
const { status, position } = fromFlashState({
|
||||
type: this.props.type,
|
||||
percentage,
|
||||
position: this.props.position,
|
||||
});
|
||||
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,76 @@
|
||||
/*
|
||||
* 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
|
207
lib/gui/app/components/safe-webview/safe-webview.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* 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 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 = this.didFailLoad.bind(this);
|
||||
this.didGetResponseDetails = this.didGetResponseDetails.bind(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);
|
||||
}
|
||||
}
|
||||
}
|
162
lib/gui/app/components/settings/settings.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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 os from 'os';
|
||||
import * as React from 'react';
|
||||
import { Flex, Checkbox, 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';
|
||||
|
||||
const platform = os.platform();
|
||||
|
||||
interface Setting {
|
||||
name: string;
|
||||
label: string | JSX.Element;
|
||||
options?: {
|
||||
description: string;
|
||||
confirmLabel: string;
|
||||
};
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
async function getSettingsList(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||
},
|
||||
{
|
||||
name: 'unmountOnSuccess',
|
||||
/**
|
||||
* On Windows, "Unmounting" basically means "ejecting".
|
||||
* On top of that, Windows users are usually not even
|
||||
* familiar with the meaning of "unmount", which comes
|
||||
* from the UNIX world.
|
||||
*/
|
||||
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
|
||||
},
|
||||
{
|
||||
name: 'validateWriteOnSuccess',
|
||||
label: 'Validate write on success',
|
||||
},
|
||||
{
|
||||
name: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
hide: ['rpm', 'deb'].includes(packageType),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
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,
|
||||
options?: Setting['options'],
|
||||
) => {
|
||||
const value = currentSettings[setting];
|
||||
const dangerous = options !== undefined;
|
||||
|
||||
analytics.logEvent('Toggle setting', {
|
||||
setting,
|
||||
value,
|
||||
dangerous,
|
||||
});
|
||||
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Txt fontSize={24} mb={24}>
|
||||
Settings
|
||||
</Txt>
|
||||
}
|
||||
done={() => toggleModal(false)}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
{settingsList.map((setting: Setting, i: number) => {
|
||||
return setting.hide ? null : (
|
||||
<Flex key={setting.name} mb={14}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
label={setting.label}
|
||||
checked={currentSettings[setting.name]}
|
||||
onChange={() => toggleSetting(setting.name, setting.options)}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
613
lib/gui/app/components/source-selector/source-selector.tsx
Normal file
@@ -0,0 +1,613 @@
|
||||
/*
|
||||
* 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 { 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 } 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,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
} from '../../styled-components';
|
||||
import { colors } from '../../theme';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import URLSelector from '../url-selector/url-selector';
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
|
||||
const isURL = (imagePath: string) =>
|
||||
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||
|
||||
// TODO move these styles to rendition
|
||||
const ModalText = styled.p`
|
||||
a {
|
||||
color: rgb(0, 174, 239);
|
||||
|
||||
&:hover {
|
||||
color: rgb(0, 139, 191);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function getState() {
|
||||
return {
|
||||
hasImage: selectionState.hasImage(),
|
||||
imageName: selectionState.getImageName(),
|
||||
imageSize: selectionState.getImageSize(),
|
||||
};
|
||||
}
|
||||
|
||||
function isString(value: any): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
// 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) {
|
||||
await this.selectSource(
|
||||
imagePath,
|
||||
isURL(imagePath) ? sourceDestination.Http : sourceDestination.File,
|
||||
).promise;
|
||||
}
|
||||
|
||||
private async createSource(selected: string, SourceType: Source) {
|
||||
try {
|
||||
selected = await replaceWindowsNetworkDriveLetter(selected);
|
||||
} catch (error) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
|
||||
if (SourceType === sourceDestination.File) {
|
||||
return new sourceDestination.File({
|
||||
path: selected,
|
||||
});
|
||||
}
|
||||
return new sourceDestination.Http({ url: selected });
|
||||
}
|
||||
|
||||
private reselectSource() {
|
||||
analytics.logEvent('Reselect image', {
|
||||
previousImage: selectionState.getImage(),
|
||||
});
|
||||
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
|
||||
private selectSource(
|
||||
selected: string | DrivelistDrive,
|
||||
SourceType: Source,
|
||||
): { 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(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);
|
||||
|
||||
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) {
|
||||
analytics.logEvent('Missing partition table', { metadata });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.missingPartitionTable(),
|
||||
title: 'Missing partition table',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleError(
|
||||
'Error opening source',
|
||||
sourcePath,
|
||||
messages.error.openSource(sourcePath, error.message),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
} catch (error) {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
} else {
|
||||
metadata = {
|
||||
path: selected.device,
|
||||
displayName: selected.displayName,
|
||||
description: selected.displayName,
|
||||
size: selected.size as SourceMetadata['size'],
|
||||
SourceType: sourceDestination.BlockDevice,
|
||||
drive: selected,
|
||||
};
|
||||
}
|
||||
|
||||
if (metadata !== undefined) {
|
||||
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');
|
||||
|
||||
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) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
}
|
||||
|
||||
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.getImagePath(),
|
||||
});
|
||||
|
||||
this.setState({
|
||||
showImageDetails: true,
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaultFlowActive(defaultFlowActive: boolean) {
|
||||
this.setState({ defaultFlowActive });
|
||||
}
|
||||
|
||||
// TODO add a visual change when dragging a file over the selector
|
||||
public render() {
|
||||
const { flashing } = this.props;
|
||||
const { showImageDetails, showURLSelector, showDriveSelector } = 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 ? (
|
||||
<>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={() => this.showSelectedImageDetails()}
|
||||
tooltip={imageName || imageBasename}
|
||||
>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
</StepNameButton>
|
||||
{!flashing && (
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={() => this.reselectSource()}
|
||||
>
|
||||
Remove
|
||||
</ChangeButton>
|
||||
)}
|
||||
{!_.isNil(imageSize) && (
|
||||
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FlowSelector
|
||||
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
|
||||
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) => {
|
||||
// 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,
|
||||
));
|
||||
await promise;
|
||||
}
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDriveSelector && (
|
||||
<DriveSelector
|
||||
multipleSelection={false}
|
||||
titleLabel="Select source"
|
||||
emptyListLabel="Plug a source"
|
||||
cancel={() => {
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}}
|
||||
done={async (drives: DrivelistDrive[]) => {
|
||||
if (drives.length) {
|
||||
await this.selectSource(
|
||||
drives[0],
|
||||
sourceDestination.BlockDevice,
|
||||
);
|
||||
}
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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%;
|
||||
}
|
||||
}
|
74
lib/gui/app/components/svg-icon/svg-icon.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2018 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
const domParser = new window.DOMParser();
|
||||
|
||||
const DEFAULT_SIZE = '40px';
|
||||
|
||||
/**
|
||||
* @summary Try to parse SVG contents and return it data encoded
|
||||
*
|
||||
*/
|
||||
function tryParseSVGContents(contents?: string): string | undefined {
|
||||
if (contents === undefined) {
|
||||
return;
|
||||
}
|
||||
const doc = domParser.parseFromString(contents, 'image/svg+xml');
|
||||
const parserError = doc.querySelector('parsererror');
|
||||
const svg = doc.querySelector('svg');
|
||||
if (!parserError && svg) {
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`;
|
||||
}
|
||||
}
|
||||
|
||||
interface SVGIconProps {
|
||||
// Optional string representing the SVG contents to be tried
|
||||
contents?: string;
|
||||
// Fallback SVG element to show if `contents` is invalid/undefined
|
||||
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
|
||||
// SVG image width unit
|
||||
width?: string;
|
||||
// SVG image height unit
|
||||
height?: string;
|
||||
// Should the element visually appear grayed out and disabled?
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary SVG element that takes file contents
|
||||
*/
|
||||
export class SVGIcon extends React.PureComponent<SVGIconProps> {
|
||||
public render() {
|
||||
const svgData = tryParseSVGContents(this.props.contents);
|
||||
const { width, height, style = {} } = this.props;
|
||||
style.width = width || DEFAULT_SIZE;
|
||||
style.height = height || DEFAULT_SIZE;
|
||||
if (svgData !== undefined) {
|
||||
return (
|
||||
<img
|
||||
className={this.props.disabled ? 'disabled' : ''}
|
||||
style={style}
|
||||
src={svgData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const { fallback: FallbackSVG } = this.props;
|
||||
return <FallbackSVG style={style} />;
|
||||
}
|
||||
}
|
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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 * as React from 'react';
|
||||
import { Flex, FlexProps, Txt } from 'rendition';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
DriveStatus,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getSelectedDrives } from '../../models/selection-state';
|
||||
import {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
interface TargetSelectorProps {
|
||||
targets: any[];
|
||||
disabled: boolean;
|
||||
openDriveSelector: () => void;
|
||||
reselectDrive: () => void;
|
||||
flashing: boolean;
|
||||
show: boolean;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
function getDriveWarning(status: DriveStatus) {
|
||||
switch (status.message) {
|
||||
case compatibility.containsImage():
|
||||
return warning.sourceDrive();
|
||||
case compatibility.largeDrive():
|
||||
return warning.largeDriveSize();
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const DriveCompatibilityWarning = ({
|
||||
warnings,
|
||||
...props
|
||||
}: {
|
||||
warnings: string[];
|
||||
} & FlexProps) => {
|
||||
const systemDrive = warnings.find(
|
||||
(message) => message === warning.systemDrive(),
|
||||
);
|
||||
return (
|
||||
<Flex tooltip={warnings.join(', ')} {...props}>
|
||||
<ExclamationTriangleSvg
|
||||
fill={systemDrive ? '#fca321' : '#8f9297'}
|
||||
height="1em"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
const targets = getSelectedDrives();
|
||||
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0];
|
||||
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
||||
getDriveWarning,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{warnings.length > 0 && (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
)}
|
||||
{middleEllipsis(target.description, 20)}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
|
||||
Change
|
||||
</ChangeButton>
|
||||
)}
|
||||
{target.size != null && (
|
||||
<DetailsText>{prettyBytes(target.size)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (targets.length > 1) {
|
||||
const targetsTemplate = [];
|
||||
for (const target of targets) {
|
||||
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
||||
getDriveWarning,
|
||||
);
|
||||
targetsTemplate.push(
|
||||
<DetailsText
|
||||
key={target.device}
|
||||
tooltip={`${target.description} ${target.displayName} ${
|
||||
target.size != null ? prettyBytes(target.size) : ''
|
||||
}`}
|
||||
px={21}
|
||||
>
|
||||
{warnings.length > 0 ? (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
) : null}
|
||||
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
|
||||
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
|
||||
</DetailsText>,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{targets.length} Targets
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
|
||||
Change
|
||||
</ChangeButton>
|
||||
)}
|
||||
{targetsTemplate}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StepButton
|
||||
primary
|
||||
tabIndex={targets.length > 0 ? -1 : 2}
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
Select target
|
||||
</StepButton>
|
||||
);
|
||||
}
|
180
lib/gui/app/components/target-selector/target-selector.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { scanner } from 'etcher-sdk';
|
||||
import * as React from 'react';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import {
|
||||
DriveSelector,
|
||||
DriveSelectorProps,
|
||||
} from '../drive-selector/drive-selector';
|
||||
import {
|
||||
isDriveSelected,
|
||||
getImage,
|
||||
getSelectedDrives,
|
||||
deselectDrive,
|
||||
selectDrive,
|
||||
} from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { TargetSelectorButton } from './target-selector-button';
|
||||
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import { warning } from '../../../../shared/messages';
|
||||
|
||||
export const getDriveListLabel = () => {
|
||||
return getSelectedDrives()
|
||||
.map((drive: any) => {
|
||||
return `${drive.description} (${drive.displayName})`;
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const shouldShowDrivesButton = () => {
|
||||
return !settings.getSync('disableExplicitDriveSelection');
|
||||
};
|
||||
|
||||
const getDriveSelectionStateSlice = () => ({
|
||||
showDrivesButton: shouldShowDrivesButton(),
|
||||
driveListLabel: getDriveListLabel(),
|
||||
targets: getSelectedDrives(),
|
||||
image: getImage(),
|
||||
});
|
||||
|
||||
export const TargetSelectorModal = (
|
||||
props: Omit<
|
||||
DriveSelectorProps,
|
||||
'titleLabel' | 'emptyListLabel' | 'multipleSelection'
|
||||
>,
|
||||
) => (
|
||||
<DriveSelector
|
||||
multipleSelection={true}
|
||||
titleLabel="Select target"
|
||||
emptyListLabel="Plug a target drive"
|
||||
showWarnings={true}
|
||||
selectedList={getSelectedDrives()}
|
||||
updateSelectedList={getSelectedDrives}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const selectAllTargets = (
|
||||
modalTargets: scanner.adapters.DrivelistDrive[],
|
||||
) => {
|
||||
const selectedDrivesFromState = getSelectedDrives();
|
||||
const deselected = selectedDrivesFromState.filter(
|
||||
(drive) =>
|
||||
!modalTargets.find((modalTarget) => modalTarget.device === drive.device),
|
||||
);
|
||||
// deselect drives
|
||||
deselected.forEach((drive) => {
|
||||
analytics.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: true,
|
||||
});
|
||||
deselectDrive(drive.device);
|
||||
});
|
||||
// select drives
|
||||
modalTargets.forEach((drive) => {
|
||||
// Don't send events for drives that were already selected
|
||||
if (!isDriveSelected(drive.device)) {
|
||||
analytics.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: false,
|
||||
});
|
||||
}
|
||||
selectDrive(drive.device);
|
||||
});
|
||||
};
|
||||
|
||||
interface TargetSelectorProps {
|
||||
disabled: boolean;
|
||||
hasDrive: boolean;
|
||||
flashing: boolean;
|
||||
}
|
||||
|
||||
export const TargetSelector = ({
|
||||
disabled,
|
||||
hasDrive,
|
||||
flashing,
|
||||
}: TargetSelectorProps) => {
|
||||
// TODO: inject these from redux-connector
|
||||
const [
|
||||
{ showDrivesButton, driveListLabel, targets },
|
||||
setStateSlice,
|
||||
] = React.useState(getDriveSelectionStateSlice());
|
||||
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
||||
false,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
return observe(() => {
|
||||
setStateSlice(getDriveSelectionStateSlice());
|
||||
});
|
||||
}, []);
|
||||
|
||||
const hasSystemDrives = targets.some((target) => target.isSystem);
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="center">
|
||||
<DriveSvg
|
||||
className={disabled ? 'disabled' : ''}
|
||||
width="40px"
|
||||
style={{
|
||||
marginBottom: 30,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TargetSelectorButton
|
||||
disabled={disabled}
|
||||
show={!hasDrive && showDrivesButton}
|
||||
tooltip={driveListLabel}
|
||||
openDriveSelector={() => {
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
reselectDrive={() => {
|
||||
analytics.logEvent('Reselect drive');
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
flashing={flashing}
|
||||
targets={targets}
|
||||
/>
|
||||
|
||||
{hasSystemDrives ? (
|
||||
<Txt
|
||||
color="#fca321"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '25px',
|
||||
}}
|
||||
>
|
||||
Warning: {warning.systemDrive()}
|
||||
</Txt>
|
||||
) : null}
|
||||
|
||||
{showTargetSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
cancel={() => setShowTargetSelectorModal(false)}
|
||||
done={(modalTargets) => {
|
||||
selectAllTargets(modalTargets);
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
@@ -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 _ = require('lodash')
|
||||
|
||||
module.exports = function (ModalService) {
|
||||
/**
|
||||
* @summary Open the tooltip modal
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - tooltip options
|
||||
* @param {String} options.title - tooltip title
|
||||
* @param {String} options.message - tooltip message
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* TooltipModalService.show({
|
||||
* title: 'Important tooltip',
|
||||
* message: 'Tooltip contents'
|
||||
* });
|
||||
*/
|
||||
this.show = (options) => {
|
||||
return ModalService.open({
|
||||
name: 'tooltip',
|
||||
template: require('../templates/tooltip-modal.tpl.html'),
|
||||
controller: 'TooltipModalController as modal',
|
||||
size: 'tooltip-modal',
|
||||
resolve: {
|
||||
tooltipData: _.constant(options)
|
||||
}
|
||||
}).result
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ ::modal.data.title }}</h4>
|
||||
<button class="close" ng-click="modal.closeModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">{{ ::modal.data.message }}</div>
|
@@ -1,32 +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.TooltipModal
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.TooltipModal'
|
||||
const TooltipModal = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal')
|
||||
])
|
||||
|
||||
TooltipModal.controller('TooltipModalController', require('./controllers/tooltip-modal'))
|
||||
TooltipModal.service('TooltipModalService', require('./services/tooltip-modal'))
|
||||
|
||||
module.exports = MODULE_NAME
|
@@ -1,151 +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 electron = require('electron')
|
||||
const Bluebird = require('bluebird')
|
||||
const _ = require('lodash')
|
||||
const settings = require('../models/settings')
|
||||
const analytics = require('../modules/analytics')
|
||||
const units = require('../../../shared/units')
|
||||
const release = require('../../../shared/release')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
|
||||
/**
|
||||
* @summary The number of days the update notifier can be put to sleep
|
||||
* @constant
|
||||
* @private
|
||||
* @type {Number}
|
||||
*/
|
||||
exports.UPDATE_NOTIFIER_SLEEP_DAYS = packageJSON.updates.sleepDays
|
||||
|
||||
/**
|
||||
* @summary The current Electron browser window
|
||||
* @constant
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
const currentWindow = electron.remote.getCurrentWindow()
|
||||
|
||||
/**
|
||||
* @summary Determine if it's time to check for updates
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {Number} [options.lastSleptUpdateNotifier] - last slept update notifier time
|
||||
* @param {String} [options.lastSleptUpdateNotifierVersion] - last slept update notifier version
|
||||
* @param {String} options.currentVersion - current version
|
||||
* @returns {Boolean} should check for updates
|
||||
*
|
||||
* @example
|
||||
* if (updateNotifier.shouldCheckForUpdates({
|
||||
* lastSleptUpdateNotifier: Date.now(),
|
||||
* lastSleptUpdateNotifierVersion: '1.0.0',
|
||||
* currentVersion: '1.0.0'
|
||||
* })) {
|
||||
* console.log('We should check for updates!');
|
||||
* }
|
||||
*/
|
||||
exports.shouldCheckForUpdates = (options) => {
|
||||
_.defaults(options, {
|
||||
lastSleptUpdateNotifierVersion: options.currentVersion
|
||||
})
|
||||
|
||||
if (_.some([
|
||||
!options.lastSleptUpdateNotifier,
|
||||
!release.isStableRelease(options.currentVersion),
|
||||
options.currentVersion !== options.lastSleptUpdateNotifierVersion
|
||||
])) {
|
||||
return true
|
||||
}
|
||||
|
||||
return Date.now() - options.lastSleptUpdateNotifier > units.daysToMilliseconds(this.UPDATE_NOTIFIER_SLEEP_DAYS)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Open the update notifier widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} version - version
|
||||
* @param {Object} [options] - options
|
||||
* @param {Boolean} [options.allowSleepUpdateCheck=true] - allow sleeping the update check
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* updateNotifier.notify('1.0.0-beta.16', {
|
||||
* allowSleepUpdateCheck: true
|
||||
* });
|
||||
*/
|
||||
exports.notify = (version, options = {}) => {
|
||||
const BUTTONS = [
|
||||
'Download',
|
||||
'Skip'
|
||||
]
|
||||
|
||||
const BUTTON_CONFIRMATION_INDEX = _.indexOf(BUTTONS, _.first(BUTTONS))
|
||||
const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, _.last(BUTTONS))
|
||||
|
||||
const dialogOptions = {
|
||||
type: 'info',
|
||||
buttons: BUTTONS,
|
||||
defaultId: BUTTON_CONFIRMATION_INDEX,
|
||||
cancelId: BUTTON_REJECTION_INDEX,
|
||||
title: 'New Update Available!',
|
||||
message: `Etcher ${version} is available for download`
|
||||
}
|
||||
|
||||
if (_.get(options, [ 'allowSleepUpdateCheck' ], true)) {
|
||||
_.merge(dialogOptions, {
|
||||
checkboxLabel: `Remind me again in ${this.UPDATE_NOTIFIER_SLEEP_DAYS} days`,
|
||||
checkboxChecked: false
|
||||
})
|
||||
}
|
||||
|
||||
return new Bluebird((resolve) => {
|
||||
electron.remote.dialog.showMessageBox(currentWindow, dialogOptions, (response, checkboxChecked) => {
|
||||
return resolve({
|
||||
agreed: response === BUTTON_CONFIRMATION_INDEX,
|
||||
sleepUpdateCheck: checkboxChecked || false
|
||||
})
|
||||
})
|
||||
}).tap((results) => {
|
||||
// Only update the last slept update timestamp if the
|
||||
// user ticked the "Remind me again in ..." checkbox,
|
||||
// but didn't agree.
|
||||
if (results.sleepUpdateCheck && !results.agreed) {
|
||||
return Bluebird.all([
|
||||
settings.set('lastSleptUpdateNotifier', Date.now()),
|
||||
settings.set('lastSleptUpdateNotifierVersion', packageJSON.version)
|
||||
])
|
||||
}
|
||||
|
||||
return Bluebird.resolve()
|
||||
}).then((results) => {
|
||||
analytics.logEvent('Close update modal', {
|
||||
sleepUpdateCheck: results.sleepUpdateCheck,
|
||||
notifyVersion: version,
|
||||
currentVersion: packageJSON.version,
|
||||
agreed: results.agreed
|
||||
})
|
||||
|
||||
if (results.agreed) {
|
||||
electron.shell.openExternal('https://etcher.io?ref=etcher_update')
|
||||
}
|
||||
})
|
||||
}
|
167
lib/gui/app/components/url-selector/url-selector.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import Checkbox from 'rendition/dist_esm5/components/Checkbox';
|
||||
import { Flex } from 'rendition/dist_esm5/components/Flex';
|
||||
import Input from 'rendition/dist_esm5/components/Input';
|
||||
import Link from 'rendition/dist_esm5/components/Link';
|
||||
import RadioButton from 'rendition/dist_esm5/components/RadioButton';
|
||||
import Txt from 'rendition/dist_esm5/components/Txt';
|
||||
|
||||
import * as settings from '../../models/settings';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
import { openDialog } from '../../os/dialog';
|
||||
import { startEllipsis } from '../../utils/start-ellipsis';
|
||||
|
||||
const RECENT_URL_IMAGES_KEY = 'recentUrlImages';
|
||||
const SAVE_IMAGE_AFTER_FLASH_KEY = 'saveUrlImage';
|
||||
const SAVE_IMAGE_AFTER_FLASH_PATH_KEY = 'saveUrlImageTo';
|
||||
|
||||
function normalizeRecentUrlImages(urls: any[]): URL[] {
|
||||
if (!Array.isArray(urls)) {
|
||||
urls = [];
|
||||
}
|
||||
urls = urls
|
||||
.map((url) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (error) {
|
||||
// Invalid URL, skip
|
||||
}
|
||||
})
|
||||
.filter((url) => url !== undefined);
|
||||
urls = uniqBy(urls, (url) => url.href);
|
||||
return urls.slice(-5);
|
||||
}
|
||||
|
||||
function getRecentUrlImages(): URL[] {
|
||||
let urls = [];
|
||||
try {
|
||||
urls = JSON.parse(localStorage.getItem(RECENT_URL_IMAGES_KEY) || '[]');
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return normalizeRecentUrlImages(urls);
|
||||
}
|
||||
|
||||
function setRecentUrlImages(urls: string[]) {
|
||||
localStorage.setItem(RECENT_URL_IMAGES_KEY, JSON.stringify(urls));
|
||||
}
|
||||
|
||||
export const URLSelector = ({
|
||||
done,
|
||||
cancel,
|
||||
}: {
|
||||
done: (imageURL: string) => void;
|
||||
cancel: () => void;
|
||||
}) => {
|
||||
const [imageURL, setImageURL] = React.useState('');
|
||||
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [saveImage, setSaveImage] = React.useState(false);
|
||||
const [saveImagePath, setSaveImagePath] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
const fetchRecentUrlImages = async () => {
|
||||
const recentUrlImages: URL[] = await getRecentUrlImages();
|
||||
setRecentImages(recentUrlImages);
|
||||
};
|
||||
const getSaveImageSettings = async () => {
|
||||
const saveUrlImage: boolean = await settings.get(
|
||||
SAVE_IMAGE_AFTER_FLASH_KEY,
|
||||
);
|
||||
const saveUrlImageToPath: string = await settings.get(
|
||||
SAVE_IMAGE_AFTER_FLASH_PATH_KEY,
|
||||
);
|
||||
setSaveImage(saveUrlImage);
|
||||
setSaveImagePath(saveUrlImageToPath);
|
||||
};
|
||||
fetchRecentUrlImages();
|
||||
getSaveImageSettings();
|
||||
}, []);
|
||||
return (
|
||||
<Modal
|
||||
title="Use Image URL"
|
||||
cancel={cancel}
|
||||
primaryButtonProps={{
|
||||
className: loading || !imageURL ? 'disabled' : '',
|
||||
}}
|
||||
done={async () => {
|
||||
setLoading(true);
|
||||
const urlStrings = recentImages
|
||||
.map((url: URL) => url.href)
|
||||
.concat(imageURL);
|
||||
setRecentUrlImages(urlStrings);
|
||||
await done(imageURL);
|
||||
}}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
<Flex mb="16px" width="100%" height="auto" flexDirection="column">
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Flex alignItems="flex-end">
|
||||
<Checkbox
|
||||
mt="16px"
|
||||
checked={saveImage}
|
||||
onChange={(evt) => {
|
||||
const value = evt.target.checked;
|
||||
setSaveImage(value);
|
||||
settings
|
||||
.set(SAVE_IMAGE_AFTER_FLASH_KEY, value)
|
||||
.then(() => setSaveImage(value));
|
||||
}}
|
||||
label={<>Save file to: </>}
|
||||
/>
|
||||
<Link
|
||||
disabled={!saveImage}
|
||||
onClick={async () => {
|
||||
if (saveImage) {
|
||||
const folder = await openDialog('openDirectory');
|
||||
if (folder) {
|
||||
await settings.set(SAVE_IMAGE_AFTER_FLASH_PATH_KEY, folder);
|
||||
setSaveImagePath(folder);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{startEllipsis(saveImagePath, 20)}
|
||||
</Link>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{recentImages.length > 0 && (
|
||||
<Flex flexDirection="column" height="58%">
|
||||
<Txt fontSize={18} mb="10px">
|
||||
Recent
|
||||
</Txt>
|
||||
<ScrollableFlex flexDirection="column" p="0">
|
||||
{recentImages
|
||||
.map((recent, i) => (
|
||||
<RadioButton
|
||||
mb={i !== 0 ? '6px' : '0'}
|
||||
key={recent.href}
|
||||
checked={imageURL === recent.href}
|
||||
label={`${recent.pathname.split('/').pop()} - ${
|
||||
recent.href
|
||||
}`}
|
||||
onChange={() => {
|
||||
setImageURL(recent.href);
|
||||
}}
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
/>
|
||||
))
|
||||
.reverse()}
|
||||
</ScrollableFlex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default URLSelector;
|
@@ -1,50 +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 ($uibModalInstance, options) {
|
||||
/**
|
||||
* @summary Modal options
|
||||
* @type {Object}
|
||||
* @public
|
||||
*/
|
||||
this.options = options
|
||||
|
||||
/**
|
||||
* @summary Reject the warning prompt
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* WarningModalController.reject();
|
||||
*/
|
||||
this.reject = () => {
|
||||
$uibModalInstance.close(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Accept the warning prompt
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* WarningModalController.accept();
|
||||
*/
|
||||
this.accept = () => {
|
||||
$uibModalInstance.close(true)
|
||||
}
|
||||
}
|
@@ -1,52 +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')
|
||||
|
||||
module.exports = function ($sce, ModalService) {
|
||||
/**
|
||||
* @summary Display the warning modal
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.description - danger message
|
||||
* @param {String} options.confirmationLabel - confirmation button text
|
||||
* @param {String} options.rejectionLabel - rejection button text
|
||||
* @fulfil {Boolean} - whether the user accepted or rejected the warning
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* WarningModalService.display({
|
||||
* description: 'Don\'t do this!',
|
||||
* confirmationLabel: 'Yes, continue!'
|
||||
* });
|
||||
*/
|
||||
this.display = (options = {}) => {
|
||||
options.description = $sce.trustAsHtml(options.description)
|
||||
return ModalService.open({
|
||||
name: 'warning',
|
||||
template: require('../templates/warning-modal.tpl.html'),
|
||||
controller: 'WarningModalController as modal',
|
||||
size: 'warning-modal',
|
||||
resolve: {
|
||||
options: _.constant(options)
|
||||
}
|
||||
}).result
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
<span>Attention</span>
|
||||
</h4>
|
||||
<button class="close"
|
||||
tabindex="11"
|
||||
ng-click="modal.reject()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="modal.options.description"></p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="modal-menu">
|
||||
<button class="button button-danger button-block"
|
||||
tabindex="13"
|
||||
ng-click="modal.accept()">{{ ::modal.options.confirmationLabel }}</button>
|
||||
<button ng-if="modal.options.rejectionLabel" class="button button-block"
|
||||
tabindex="12"
|
||||
ng-click="modal.reject()">{{ ::modal.options.rejectionLabel }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,32 +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.WarningModal
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.WarningModal'
|
||||
const WarningModal = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal')
|
||||
])
|
||||
|
||||
WarningModal.controller('WarningModalController', require('./controllers/warning-modal'))
|
||||
WarningModal.service('WarningModalService', require('./services/warning-modal'))
|
||||
|
||||
module.exports = MODULE_NAME
|
BIN
lib/gui/app/css/fonts/SourceSansPro-Regular.ttf
Normal file
BIN
lib/gui/app/css/fonts/SourceSansPro-SemiBold.ttf
Normal file
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* 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.
|
||||
@@ -14,36 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* Prevent text selection */
|
||||
body {
|
||||
-webkit-user-select: none;
|
||||
@font-face {
|
||||
font-family: "SourceSansPro";
|
||||
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
/* Allow window to be dragged from anywhere */
|
||||
* {
|
||||
-webkit-app-region: drag;
|
||||
@font-face {
|
||||
font-family: "SourceSansPro";
|
||||
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
button,
|
||||
a,
|
||||
input {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Prevent WebView bounce effect in OS X */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
||||
/* Prevent white flash when running application */
|
||||
background-color: #4d5057;
|
||||
|
||||
/* Prevent WebView bounce effect in OS X */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Prevent text selection */
|
||||
body {
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@@ -51,11 +51,16 @@ body {
|
||||
a:focus,
|
||||
input:focus,
|
||||
button:focus,
|
||||
[tabindex]:focus {
|
||||
[tabindex]:focus,
|
||||
input[type="checkbox"] + div {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Titles don't have margins on desktop apps */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#rendition-tooltip-root > div {
|
||||
font-family: "SourceSansPro", sans-serif;
|
||||
}
|
@@ -3,73 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Etcher</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../node_modules/flexboxgrid/dist/flexboxgrid.css">
|
||||
<link rel="stylesheet" type="text/css" href="../css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="../css/desktop.css">
|
||||
<link rel="stylesheet" type="text/css" href="../css/angular.css">
|
||||
<script src="../../../generated/gui.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="section-header" ng-controller="HeaderController as header">
|
||||
<button class="button button-link"
|
||||
ng-click="header.openHelpPage()"
|
||||
tabindex="4">
|
||||
<span class="glyphicon glyphicon-question-sign"></span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link"
|
||||
ui-sref="settings"
|
||||
hide-if-state="settings"
|
||||
tabindex="5">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link"
|
||||
tabindex="5"
|
||||
ui-sref="main"
|
||||
show-if-state="settings">
|
||||
<span class="glyphicon glyphicon-chevron-left"></span> Back
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main class="wrapper" ui-view></main>
|
||||
|
||||
<footer class="section-footer" ng-controller="StateController as state"
|
||||
ng-hide="state.currentName === 'success'">
|
||||
<span os-open-external="https://etcher.io?ref=etcher_footer"
|
||||
tabindex="100">
|
||||
<svg-icon paths="[ '../../assets/etcher.svg' ]"
|
||||
width="'83px'"
|
||||
height="'13px'"></svg-icon>
|
||||
</span>
|
||||
|
||||
<span class="caption">
|
||||
is <span class="caption"
|
||||
tabindex="101"
|
||||
os-open-external="https://github.com/resin-io/etcher">an open source project</span> by
|
||||
</span>
|
||||
|
||||
<span os-open-external="https://resin.io?ref=etcher"
|
||||
tabindex="102">
|
||||
<svg-icon paths="[ '../../assets/resin.svg' ]"
|
||||
width="'79px'"
|
||||
height="'23px'"></svg-icon>
|
||||
</span>
|
||||
|
||||
<span class="caption footer-right"
|
||||
tabindex="103"
|
||||
manifest-bind="version"
|
||||
os-open-external="https://github.com/resin-io/etcher/blob/master/CHANGELOG.md"></span>
|
||||
</footer>
|
||||
|
||||
<div class="section-loader"
|
||||
ng-controller="StateController as state"
|
||||
ng-class="{
|
||||
isFinish: state.currentName === 'success'
|
||||
}">
|
||||
<safe-webview
|
||||
src="'https://etcher.io/success-banner/'"
|
||||
refresh-now="state.previousName === 'success'"></safe-webview>
|
||||
</div>
|
||||
<main id="main"></main>
|
||||
<script src="gui.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* 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.
|
||||
@@ -14,22 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.label {
|
||||
font-size: 9px;
|
||||
margin-right: 4.5px;
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
export function hasAvailableDrives() {
|
||||
return getDrives().length > 0;
|
||||
}
|
||||
|
||||
.label-big {
|
||||
font-size: 11px;
|
||||
padding: 8px 25px;
|
||||
export function setDrives(drives: any[]) {
|
||||
store.dispatch({
|
||||
type: Actions.SET_AVAILABLE_TARGETS,
|
||||
data: drives,
|
||||
});
|
||||
}
|
||||
|
||||
.label-inset {
|
||||
background-color: darken($palette-theme-dark-background, 10%);
|
||||
color: darken($palette-theme-dark-foreground, 43%);
|
||||
}
|
||||
|
||||
.label-danger {
|
||||
background-color: $palette-theme-danger-background;
|
||||
color: $palette-theme-danger-foreground;
|
||||
export function getDrives(): DrivelistDrive[] {
|
||||
return store.getState().toJS().availableDrives;
|
||||
}
|
152
lib/gui/app/models/flash-state.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { bytesToMegabytes } from '../../../shared/units';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
/**
|
||||
* @summary Reset flash state
|
||||
*/
|
||||
export function resetState() {
|
||||
store.dispatch({
|
||||
type: Actions.RESET_FLASH_STATE,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if currently flashing
|
||||
*/
|
||||
export function isFlashing(): boolean {
|
||||
return store.getState().toJS().isFlashing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Set the flashing flag
|
||||
*
|
||||
* @description
|
||||
* The flag is used to signify that we're going to
|
||||
* start a flash process.
|
||||
*/
|
||||
export function setFlashingFlag() {
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASHING_FLAG,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Unset the flashing flag
|
||||
*
|
||||
* @description
|
||||
* The flag is used to signify that the write process ended.
|
||||
*/
|
||||
export function unsetFlashingFlag(results: {
|
||||
cancelled?: boolean;
|
||||
sourceChecksum?: string;
|
||||
errorCode?: string | number;
|
||||
}) {
|
||||
store.dispatch({
|
||||
type: Actions.UNSET_FLASHING_FLAG,
|
||||
data: results,
|
||||
});
|
||||
}
|
||||
|
||||
export function setDevicePaths(devicePaths: string[]) {
|
||||
store.dispatch({
|
||||
type: Actions.SET_DEVICE_PATHS,
|
||||
data: devicePaths,
|
||||
});
|
||||
}
|
||||
|
||||
export function addFailedDevicePath({
|
||||
device,
|
||||
error,
|
||||
}: {
|
||||
device: sdk.scanner.adapters.DrivelistDrive;
|
||||
error: Error;
|
||||
}) {
|
||||
const failedDevicePathsMap = new Map(
|
||||
store.getState().toJS().failedDevicePaths,
|
||||
);
|
||||
failedDevicePathsMap.set(device.device, {
|
||||
description: device.description,
|
||||
device: device.device,
|
||||
devicePath: device.devicePath,
|
||||
...error,
|
||||
});
|
||||
store.dispatch({
|
||||
type: Actions.SET_FAILED_DEVICE_PATHS,
|
||||
data: Array.from(failedDevicePathsMap),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Set the flashing state
|
||||
*/
|
||||
export function setProgressState(
|
||||
state: sdk.multiWrite.MultiDestinationProgress,
|
||||
) {
|
||||
// Preserve only one decimal place
|
||||
const PRECISION = 1;
|
||||
const data = {
|
||||
...state,
|
||||
percentage:
|
||||
state.percentage !== undefined && _.isFinite(state.percentage)
|
||||
? Math.floor(state.percentage)
|
||||
: undefined,
|
||||
|
||||
speed: _.attempt(() => {
|
||||
if (_.isFinite(state.speed)) {
|
||||
return _.round(bytesToMegabytes(state.speed), PRECISION);
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASH_STATE,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getFlashResults() {
|
||||
return store.getState().toJS().flashResults;
|
||||
}
|
||||
|
||||
export function getFlashState() {
|
||||
return store.getState().get('flashState').toJS();
|
||||
}
|
||||
|
||||
export function wasLastFlashCancelled() {
|
||||
return _.get(getFlashResults(), ['cancelled'], false);
|
||||
}
|
||||
|
||||
export function getLastFlashSourceChecksum(): string {
|
||||
return getFlashResults().sourceChecksum;
|
||||
}
|
||||
|
||||
export function getLastFlashErrorCode() {
|
||||
return getFlashResults().errorCode;
|
||||
}
|
||||
|
||||
export function getFlashUuid() {
|
||||
return store.getState().toJS().flashUuid;
|
||||
}
|
225
lib/gui/app/models/leds.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2020 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import {
|
||||
isSourceDrive,
|
||||
DrivelistDrive,
|
||||
} from '../../../shared/drive-constraints';
|
||||
import * as settings from './settings';
|
||||
import { DEFAULT_STATE, observe } from './store';
|
||||
|
||||
const leds: Map<string, RGBLed> = new Map();
|
||||
|
||||
function setLeds(
|
||||
drivesPaths: Set<string>,
|
||||
colorOrAnimation: Color | AnimationFunction,
|
||||
frequency?: number,
|
||||
) {
|
||||
for (const path of drivesPaths) {
|
||||
const led = leds.get(path);
|
||||
if (led) {
|
||||
if (Array.isArray(colorOrAnimation)) {
|
||||
led.setStaticColor(colorOrAnimation);
|
||||
} else {
|
||||
led.setAnimation(colorOrAnimation, frequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const red: Color = [1, 0, 0];
|
||||
const green: Color = [0, 1, 0];
|
||||
const blue: Color = [0, 0, 1];
|
||||
const white: Color = [1, 1, 1];
|
||||
const black: Color = [0, 0, 0];
|
||||
const purple: Color = [0.5, 0, 0.5];
|
||||
|
||||
function createAnimationFunction(
|
||||
intensityFunction: (t: number) => number,
|
||||
color: Color,
|
||||
): AnimationFunction {
|
||||
return (t: number): Color => {
|
||||
const intensity = intensityFunction(t);
|
||||
return color.map((v) => v * intensity) as Color;
|
||||
};
|
||||
}
|
||||
|
||||
function blink(t: number) {
|
||||
return Math.floor(t / 1000) % 2;
|
||||
}
|
||||
|
||||
function breathe(t: number) {
|
||||
return (1 + Math.sin(t / 1000)) / 2;
|
||||
}
|
||||
|
||||
const breatheBlue = createAnimationFunction(breathe, blue);
|
||||
const blinkGreen = createAnimationFunction(blink, green);
|
||||
const blinkPurple = createAnimationFunction(blink, purple);
|
||||
|
||||
interface LedsState {
|
||||
step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||
sourceDrive: string | undefined;
|
||||
availableDrives: string[];
|
||||
selectedDrives: string[];
|
||||
failedDrives: string[];
|
||||
}
|
||||
|
||||
// Source slot (1st slot): behaves as a target unless it is chosen as source
|
||||
// No drive: black
|
||||
// Drive plugged: blue - on
|
||||
//
|
||||
// Other slots (2 - 16):
|
||||
//
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
// | | main screen | flashing | validating | results screen |
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
// | no drive | black | black | black | black |
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
// | drive plugged | black | black | black | black |
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
// | drive selected | white | blink purple, red if failed | blink green, red if failed | green if success, red if failed |
|
||||
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
|
||||
export function updateLeds({
|
||||
step,
|
||||
sourceDrive,
|
||||
availableDrives,
|
||||
selectedDrives,
|
||||
failedDrives,
|
||||
}: LedsState) {
|
||||
const unplugged = new Set(leds.keys());
|
||||
const plugged = new Set(availableDrives);
|
||||
const selectedOk = new Set(selectedDrives);
|
||||
const selectedFailed = new Set(failedDrives);
|
||||
|
||||
// Remove selected devices from plugged set
|
||||
for (const d of selectedOk) {
|
||||
plugged.delete(d);
|
||||
}
|
||||
|
||||
// Remove plugged devices from unplugged set
|
||||
for (const d of plugged) {
|
||||
unplugged.delete(d);
|
||||
}
|
||||
|
||||
// Remove failed devices from selected set
|
||||
for (const d of selectedFailed) {
|
||||
selectedOk.delete(d);
|
||||
}
|
||||
|
||||
// Handle source slot
|
||||
if (sourceDrive !== undefined) {
|
||||
if (unplugged.has(sourceDrive)) {
|
||||
unplugged.delete(sourceDrive);
|
||||
// TODO
|
||||
setLeds(new Set([sourceDrive]), breatheBlue, 2);
|
||||
} else if (plugged.has(sourceDrive)) {
|
||||
plugged.delete(sourceDrive);
|
||||
setLeds(new Set([sourceDrive]), blue);
|
||||
}
|
||||
}
|
||||
if (step === 'main') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, white);
|
||||
setLeds(selectedFailed, white);
|
||||
} else if (step === 'flashing') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, blinkPurple, 2);
|
||||
setLeds(selectedFailed, red);
|
||||
} else if (step === 'verifying') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, blinkGreen, 2);
|
||||
setLeds(selectedFailed, red);
|
||||
} else if (step === 'finish') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, green);
|
||||
setLeds(selectedFailed, red);
|
||||
}
|
||||
}
|
||||
|
||||
interface DeviceFromState {
|
||||
devicePath?: string;
|
||||
device: string;
|
||||
}
|
||||
|
||||
let ledsState: LedsState | undefined;
|
||||
|
||||
function stateObserver(state: typeof DEFAULT_STATE) {
|
||||
const s = state.toJS();
|
||||
let step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||
if (s.isFlashing) {
|
||||
step = s.flashState.type;
|
||||
} else {
|
||||
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
|
||||
}
|
||||
const availableDrives = s.availableDrives.filter(
|
||||
(d: DeviceFromState) => d.devicePath,
|
||||
);
|
||||
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
|
||||
isSourceDrive(d, s.selection.image),
|
||||
)[0]?.devicePath;
|
||||
const availableDrivesPaths = availableDrives.map(
|
||||
(d: DeviceFromState) => d.devicePath,
|
||||
);
|
||||
let selectedDrivesPaths: string[];
|
||||
if (step === 'main') {
|
||||
selectedDrivesPaths = availableDrives
|
||||
.filter((d: DrivelistDrive) => s.selection.devices.includes(d.device))
|
||||
.map((d: DrivelistDrive) => d.devicePath);
|
||||
} else {
|
||||
selectedDrivesPaths = s.devicePaths;
|
||||
}
|
||||
const failedDevicePaths = s.failedDevicePaths.map(
|
||||
([devicePath]: [string]) => devicePath,
|
||||
);
|
||||
const newLedsState = {
|
||||
step,
|
||||
sourceDrive: sourceDrivePath,
|
||||
availableDrives: availableDrivesPaths,
|
||||
selectedDrives: selectedDrivesPaths,
|
||||
failedDrives: failedDevicePaths,
|
||||
};
|
||||
if (!_.isEqual(newLedsState, ledsState)) {
|
||||
updateLeds(newLedsState);
|
||||
ledsState = newLedsState;
|
||||
}
|
||||
}
|
||||
|
||||
export async function init(): Promise<void> {
|
||||
// ledsMapping is something like:
|
||||
// {
|
||||
// 'platform-xhci-hcd.0.auto-usb-0:1.1.1:1.0-scsi-0:0:0:0': [
|
||||
// 'led1_r',
|
||||
// 'led1_g',
|
||||
// 'led1_b',
|
||||
// ],
|
||||
// ...
|
||||
// }
|
||||
const ledsMapping: _.Dictionary<[string, string, string]> =
|
||||
(await settings.get('ledsMapping')) || {};
|
||||
if (!_.isEmpty(ledsMapping)) {
|
||||
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
|
||||
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
|
||||
}
|
||||
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
|
||||
}
|
||||
}
|
@@ -1,88 +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 Bluebird = require('bluebird')
|
||||
|
||||
/**
|
||||
* @summary Local storage settings key
|
||||
* @constant
|
||||
* @type {String}
|
||||
*/
|
||||
const LOCAL_STORAGE_SETTINGS_KEY = 'etcher-settings'
|
||||
|
||||
/**
|
||||
* @summary Read all local settings
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @fulfil {Object} - local settings
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* localSettings.readAll().then((settings) => {
|
||||
* console.log(settings);
|
||||
* });
|
||||
*/
|
||||
exports.readAll = () => {
|
||||
return Bluebird.try(() => {
|
||||
return JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_SETTINGS_KEY)) || {}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Write local settings
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} settings - settings
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* localSettings.writeAll({
|
||||
* foo: 'bar'
|
||||
* }).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.writeAll = (settings) => {
|
||||
const INDENTATION_SPACES = 2
|
||||
return Bluebird.try(() => {
|
||||
window.localStorage.setItem(LOCAL_STORAGE_SETTINGS_KEY, JSON.stringify(settings, null, INDENTATION_SPACES))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Clear the local settings
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @description
|
||||
* Exported for testing purposes
|
||||
*
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* localSettings.clear().then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.clear = () => {
|
||||
return Bluebird.try(() => {
|
||||
window.localStorage.removeItem(LOCAL_STORAGE_SETTINGS_KEY)
|
||||
})
|
||||
}
|
148
lib/gui/app/models/selection-state.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
/*
|
||||
* 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 { SourceMetadata } from '../components/source-selector/source-selector';
|
||||
|
||||
import * as availableDrives from './available-drives';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
/**
|
||||
* @summary Select a drive by its device path
|
||||
*/
|
||||
export function selectDrive(driveDevice: string) {
|
||||
store.dispatch({
|
||||
type: Actions.SELECT_TARGET,
|
||||
data: driveDevice,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Toggle drive selection
|
||||
*/
|
||||
export function toggleDrive(driveDevice: string) {
|
||||
if (isDriveSelected(driveDevice)) {
|
||||
deselectDrive(driveDevice);
|
||||
} else {
|
||||
selectDrive(driveDevice);
|
||||
}
|
||||
}
|
||||
|
||||
export function selectSource(source: SourceMetadata) {
|
||||
store.dispatch({
|
||||
type: Actions.SELECT_SOURCE,
|
||||
data: source,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get all selected drives' devices
|
||||
*/
|
||||
export function getSelectedDevices(): string[] {
|
||||
return store.getState().getIn(['selection', 'devices']).toJS();
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get all selected drive objects
|
||||
*/
|
||||
export function getSelectedDrives(): DrivelistDrive[] {
|
||||
const selectedDevices = getSelectedDevices();
|
||||
return availableDrives
|
||||
.getDrives()
|
||||
.filter((drive) => selectedDevices.includes(drive.device));
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the selected image
|
||||
*/
|
||||
export function getImage(): SourceMetadata | undefined {
|
||||
return store.getState().toJS().selection.image;
|
||||
}
|
||||
|
||||
export function getImagePath(): string | undefined {
|
||||
return store.getState().toJS().selection.image?.path;
|
||||
}
|
||||
|
||||
export function getImageSize(): number | undefined {
|
||||
return store.getState().toJS().selection.image?.size;
|
||||
}
|
||||
|
||||
export function getImageName(): string | undefined {
|
||||
return store.getState().toJS().selection.image?.name;
|
||||
}
|
||||
|
||||
export function getImageLogo(): string | undefined {
|
||||
return store.getState().toJS().selection.image?.logo;
|
||||
}
|
||||
|
||||
export function getImageSupportUrl(): string | undefined {
|
||||
return store.getState().toJS().selection.image?.supportUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if there is a selected drive
|
||||
*/
|
||||
export function hasDrive(): boolean {
|
||||
return Boolean(getSelectedDevices().length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if there is a selected image
|
||||
*/
|
||||
export function hasImage(): boolean {
|
||||
return getImage() !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Remove drive from selection
|
||||
*/
|
||||
export function deselectDrive(driveDevice: string) {
|
||||
store.dispatch({
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: driveDevice,
|
||||
});
|
||||
}
|
||||
|
||||
export function deselectImage() {
|
||||
store.dispatch({
|
||||
type: Actions.DESELECT_SOURCE,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
export function deselectAllDrives() {
|
||||
getSelectedDevices().forEach(deselectDrive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Clear selections
|
||||
*/
|
||||
export function clear() {
|
||||
deselectImage();
|
||||
deselectAllDrives();
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check whether a given device is selected.
|
||||
*/
|
||||
export function isDriveSelected(driveDevice: string) {
|
||||
if (!driveDevice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedDriveDevices = getSelectedDevices();
|
||||
return selectedDriveDevices.includes(driveDevice);
|
||||
}
|
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Models.Settings
|
||||
*/
|
||||
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const localSettings = require('./local-settings')
|
||||
const store = require('../../../shared/store')
|
||||
const errors = require('../../../shared/errors')
|
||||
|
||||
/**
|
||||
* @summary Set a settings object
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @description
|
||||
* Use this function with care, given that it will completely
|
||||
* override any existing settings in both the redux store,
|
||||
* and the local user configuration.
|
||||
*
|
||||
* This function is prepared to deal with any local configuration
|
||||
* write issues by rolling back to the previous settings if so.
|
||||
*
|
||||
* @param {Object} settings - settings
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* setSettingsObject({ foo: 'bar' }).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
const setSettingsObject = (settings) => {
|
||||
const currentSettings = exports.getAll()
|
||||
|
||||
return Bluebird.try(() => {
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_SETTINGS,
|
||||
data: settings
|
||||
})
|
||||
}).then(() => {
|
||||
// Revert the application state if writing the data
|
||||
// to the local machine was not successful
|
||||
return localSettings.writeAll(settings).catch((error) => {
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_SETTINGS,
|
||||
data: currentSettings
|
||||
})
|
||||
|
||||
throw error
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Default settings
|
||||
* @constant
|
||||
* @type {Object}
|
||||
*/
|
||||
const DEFAULT_SETTINGS = store.Defaults.get('settings').toJS()
|
||||
|
||||
/**
|
||||
* @summary Reset settings to their default values
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.reset().then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.reset = () => {
|
||||
return setSettingsObject(DEFAULT_SETTINGS)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the current settings
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} settings - settings
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.assign({
|
||||
* foo: 'bar'
|
||||
* }).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.assign = (settings) => {
|
||||
if (_.isNil(settings)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: 'Missing settings'
|
||||
}))
|
||||
}
|
||||
|
||||
return setSettingsObject(_.assign(exports.getAll(), settings))
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the application state with the local settings
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.load().then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.load = () => {
|
||||
return localSettings.readAll().then(exports.assign)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Set a setting value
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} key - setting key
|
||||
* @param {*} value - setting value
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.set('unmountOnSuccess', true).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.set = (key, value) => {
|
||||
if (_.isNil(key)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: 'Missing setting key'
|
||||
}))
|
||||
}
|
||||
|
||||
if (!_.isString(key)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: `Invalid setting key: ${key}`
|
||||
}))
|
||||
}
|
||||
|
||||
return exports.assign({
|
||||
[key]: value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a setting value
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} key - setting key
|
||||
* @returns {*} setting value
|
||||
*
|
||||
* @example
|
||||
* const value = settings.get('unmountOnSuccess');
|
||||
*/
|
||||
exports.get = (key) => {
|
||||
return _.get(exports.getAll(), [ key ])
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get all setting values
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Object} all setting values
|
||||
*
|
||||
* @example
|
||||
* const allSettings = settings.getAll();
|
||||
* console.log(allSettings.unmountOnSuccess);
|
||||
*/
|
||||
exports.getAll = () => {
|
||||
return store.getState().get('settings').toJS()
|
||||
}
|
134
lib/gui/app/models/settings.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 _debug from 'debug';
|
||||
import * as electron from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
|
||||
const debug = _debug('etcher:models:settings');
|
||||
|
||||
const JSON_INDENT = 2;
|
||||
|
||||
export const DEFAULT_WIDTH = 800;
|
||||
export const DEFAULT_HEIGHT = 480;
|
||||
|
||||
/**
|
||||
* @summary Userdata directory path
|
||||
* @description
|
||||
* Defaults to the following:
|
||||
* - `%APPDATA%/etcher` on Windows
|
||||
* - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux
|
||||
* - `~/Library/Application Support/etcher` on macOS
|
||||
* See https://electronjs.org/docs/api/app#appgetpathname
|
||||
*
|
||||
* NOTE: The ternary is due to this module being loaded both,
|
||||
* Electron's main process and renderer process
|
||||
*/
|
||||
|
||||
const app = electron.app || electron.remote.app;
|
||||
|
||||
const USER_DATA_DIR = app.getPath('userData');
|
||||
|
||||
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
const DOWNLOADS_DIR = app.getPath('downloads');
|
||||
|
||||
async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
||||
let contents = '{}';
|
||||
try {
|
||||
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
// noop
|
||||
}
|
||||
try {
|
||||
return JSON.parse(contents);
|
||||
} catch (parseError) {
|
||||
console.error(parseError);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export async function readAll() {
|
||||
return await readConfigFile(CONFIG_PATH);
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export async function writeConfigFile(
|
||||
filename: string,
|
||||
data: _.Dictionary<any>,
|
||||
): Promise<void> {
|
||||
await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT));
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true,
|
||||
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
|
||||
desktopNotifications: true,
|
||||
autoBlockmapping: true,
|
||||
decompressFirst: true,
|
||||
saveUrlImage: false,
|
||||
saveUrlImageTo: DOWNLOADS_DIR,
|
||||
};
|
||||
|
||||
const settings = _.cloneDeep(DEFAULT_SETTINGS);
|
||||
|
||||
async function load(): Promise<void> {
|
||||
debug('load');
|
||||
const loadedSettings = await readAll();
|
||||
_.assign(settings, loadedSettings);
|
||||
}
|
||||
|
||||
const loaded = load();
|
||||
|
||||
export async function set(
|
||||
key: string,
|
||||
value: any,
|
||||
writeConfigFileFn = writeConfigFile,
|
||||
): Promise<void> {
|
||||
debug('set', key, value);
|
||||
await loaded;
|
||||
const previousValue = settings[key];
|
||||
settings[key] = value;
|
||||
try {
|
||||
await writeConfigFileFn(CONFIG_PATH, settings);
|
||||
} catch (error) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(key: string): Promise<any> {
|
||||
await loaded;
|
||||
return getSync(key);
|
||||
}
|
||||
|
||||
export function getSync(key: string): any {
|
||||
return _.cloneDeep(settings[key]);
|
||||
}
|
||||
|
||||
export async function getAll() {
|
||||
debug('getAll');
|
||||
await loaded;
|
||||
return _.cloneDeep(settings);
|
||||
}
|