Compare commits
1266 Commits
v1.0.0-bet
...
react-driv
Author | SHA1 | Date | |
---|---|---|---|
![]() |
868a35337c | ||
![]() |
1398ca2931 | ||
![]() |
96c865f14a | ||
![]() |
6dbd425e89 | ||
![]() |
5b2769d0e9 | ||
![]() |
5f38cca60c | ||
![]() |
78cebdb7a4 | ||
![]() |
a6aedab0a0 | ||
![]() |
4aeccbe963 | ||
![]() |
126b3fbb40 | ||
![]() |
9ea8a6134e | ||
![]() |
3706770322 | ||
![]() |
7be07bfe8c | ||
![]() |
791c047fa1 | ||
![]() |
35ad0340b9 | ||
![]() |
0a0be3a13d | ||
![]() |
86238af380 | ||
![]() |
d10073a052 | ||
![]() |
b99b0d4bf8 | ||
![]() |
27b5b1bf10 | ||
![]() |
bab9069dee | ||
![]() |
52a3258814 | ||
![]() |
da548f59d1 | ||
![]() |
ecc500907c | ||
![]() |
724dade1f6 | ||
![]() |
c5dc869c03 | ||
![]() |
273f7e4535 | ||
![]() |
a58e060138 | ||
![]() |
ef4d2fcc72 | ||
![]() |
330c06d926 | ||
![]() |
be9c36828a | ||
![]() |
17f83135c5 | ||
![]() |
543ba51d3c | ||
![]() |
33df23fc8c | ||
![]() |
3236d6b934 | ||
![]() |
e0e7775367 | ||
![]() |
198679583c | ||
![]() |
6dae2a604f | ||
![]() |
68905c6ae4 | ||
![]() |
26630c4d64 | ||
![]() |
d382f030f0 | ||
![]() |
08fca87b2f | ||
![]() |
33441a1c5c | ||
![]() |
6d8346b13a | ||
![]() |
d9b340ca45 | ||
![]() |
ebbc52ee1f | ||
![]() |
de5bee29ef | ||
![]() |
25f843ec0b | ||
![]() |
df600a9e14 | ||
![]() |
156c25cea1 | ||
![]() |
f7dd04e3de | ||
![]() |
3036d86cfa | ||
![]() |
3fccd52884 | ||
![]() |
00640274fc | ||
![]() |
a7e8fb98b3 | ||
![]() |
bed6643437 | ||
![]() |
f815e8511f | ||
![]() |
6360fd42e7 | ||
![]() |
62a9656888 | ||
![]() |
ffb89c7e5b | ||
![]() |
aa52735006 | ||
![]() |
b65526d8ee | ||
![]() |
ea8e2999ae | ||
![]() |
0b5017f992 | ||
![]() |
8bf1bdaa04 | ||
![]() |
01eb3b1c94 | ||
![]() |
3402c9f601 | ||
![]() |
13c3518c5e | ||
![]() |
821fad27dc | ||
![]() |
50a34e2f4c | ||
![]() |
2a19b2afbe | ||
![]() |
dc92d010fb | ||
![]() |
9cb27a616a | ||
![]() |
518a0ca45b | ||
![]() |
3526a0e3c5 | ||
![]() |
6386f85258 | ||
![]() |
e80106d8f8 | ||
![]() |
1145cbc75c | ||
![]() |
e669b81072 | ||
![]() |
9d78da941b | ||
![]() |
63d0f5e2c6 | ||
![]() |
dae047eff1 | ||
![]() |
8a2db8bced | ||
![]() |
792fab20e6 | ||
![]() |
f40c0f6bd3 | ||
![]() |
ccf11b9861 | ||
![]() |
1fcde5a17c | ||
![]() |
88f543dd25 | ||
![]() |
8b6f3f6022 | ||
![]() |
294ef8045a | ||
![]() |
1f7e4c886b | ||
![]() |
63c047009f | ||
![]() |
2c5f5004cc | ||
![]() |
2fa5426cf5 | ||
![]() |
428c777402 | ||
![]() |
7e2c62c520 | ||
![]() |
3d3b4f4a46 | ||
![]() |
a543dcf166 | ||
![]() |
5de54bb6bf | ||
![]() |
d95401e614 | ||
![]() |
2c835437e9 | ||
![]() |
498e70ed2b | ||
![]() |
fce5b500bf | ||
![]() |
11def54adb | ||
![]() |
9da9e73f7a | ||
![]() |
f90cd49a6d | ||
![]() |
6e72c07190 | ||
![]() |
1997e1faeb | ||
![]() |
b33b34bd71 | ||
![]() |
6a9b739541 | ||
![]() |
6cb0bdd1a4 | ||
![]() |
b73ebb6f92 | ||
![]() |
24a83260ca | ||
![]() |
fc1c1b402b | ||
![]() |
af462b3486 | ||
![]() |
3e236996c8 | ||
![]() |
c4e84483df | ||
![]() |
e9532f2158 | ||
![]() |
15fc8ab2e7 | ||
![]() |
b39dbeb25e | ||
![]() |
e181c21f85 | ||
![]() |
db771bc2cc | ||
![]() |
0695cfb3c0 | ||
![]() |
ff3982efa4 | ||
![]() |
40de7f5d54 | ||
![]() |
18a848696f | ||
![]() |
58de7375a2 | ||
![]() |
b61109a269 | ||
![]() |
164fd8f022 | ||
![]() |
cafaa9ff22 | ||
![]() |
34c98d1dcd | ||
![]() |
ec015da795 | ||
![]() |
a21a82c64e | ||
![]() |
1aca669c62 | ||
![]() |
39573ada54 | ||
![]() |
bceb7c77d1 | ||
![]() |
68fa771905 | ||
![]() |
8a85968ec4 | ||
![]() |
03b1a2dcff | ||
![]() |
5cb231e427 | ||
![]() |
e90f6764f5 | ||
![]() |
d078055e40 | ||
![]() |
98134eb612 | ||
![]() |
b0f2dfc4fb | ||
![]() |
195f07c09f | ||
![]() |
15f87edc96 | ||
![]() |
52caae8f05 | ||
![]() |
99f7fc99da | ||
![]() |
89fc98c6f6 | ||
![]() |
0c2eb1caab | ||
![]() |
303969bdbf | ||
![]() |
8b7266a435 | ||
![]() |
fc9282fff7 | ||
![]() |
33fb79e0de | ||
![]() |
b4418660df | ||
![]() |
3560a22a77 | ||
![]() |
818b466687 | ||
![]() |
91d29c1e2c | ||
![]() |
e06fe72131 | ||
![]() |
7d715fdca0 | ||
![]() |
3cfa6988ab | ||
![]() |
1c707b2d37 | ||
![]() |
2ed4c76abf | ||
![]() |
94e91723f4 | ||
![]() |
091bddbad8 | ||
![]() |
b3f68c2638 | ||
![]() |
00c94c8efd | ||
![]() |
66b19677bf | ||
![]() |
2d4170e0d7 | ||
![]() |
7f8f38ddf1 | ||
![]() |
2e1763f19a | ||
![]() |
a5f8b3c164 | ||
![]() |
a802087009 | ||
![]() |
3b16c06f70 | ||
![]() |
a979ae3ced | ||
![]() |
02e0f40702 | ||
![]() |
a4d7853bb2 | ||
![]() |
ac463e0f65 | ||
![]() |
c187aa46b7 | ||
![]() |
e65fa9dcae | ||
![]() |
fea230cfab | ||
![]() |
90838c99fc | ||
![]() |
8e96adeda9 | ||
![]() |
3cdb0f840e | ||
![]() |
b6ad6e0a85 | ||
![]() |
e73a577452 | ||
![]() |
16e8aa2447 | ||
![]() |
1d6958a67e | ||
![]() |
136ca282eb | ||
![]() |
388fc2f7d9 | ||
![]() |
72fdb7127a | ||
![]() |
f67832644f | ||
![]() |
2614f3261c | ||
![]() |
e84204b49a | ||
![]() |
86aaa725d5 | ||
![]() |
dd583a176f | ||
![]() |
5299d958f2 | ||
![]() |
f0374cf9d9 | ||
![]() |
6b6a0d7b4f | ||
![]() |
4317892421 | ||
![]() |
8052b2adfa | ||
![]() |
d44927447a | ||
![]() |
09e6c6422d | ||
![]() |
df85ffb254 | ||
![]() |
752ba4405c | ||
![]() |
1f3a02b83d | ||
![]() |
8e372f1e93 | ||
![]() |
caeb84f58b | ||
![]() |
759722bf7d | ||
![]() |
cefdc95c9b | ||
![]() |
3be7029078 | ||
![]() |
3f16858a93 | ||
![]() |
c61c6deaa8 | ||
![]() |
90c8483df8 | ||
![]() |
d70b189cec | ||
![]() |
41a7fc4de5 | ||
![]() |
db119d5230 | ||
![]() |
c88245954d | ||
![]() |
b1ab3834b6 | ||
![]() |
34b7c1be81 | ||
![]() |
082c77586f | ||
![]() |
3b6fe7b548 | ||
![]() |
da072e7621 | ||
![]() |
4568404a70 | ||
![]() |
43319853ef | ||
![]() |
ce9f142621 | ||
![]() |
8ef3add183 | ||
![]() |
ef45696015 | ||
![]() |
a8f8c2cd85 | ||
![]() |
6d79a8e23a | ||
![]() |
d65dc6ccac | ||
![]() |
ccc9076a80 | ||
![]() |
3c007cea34 | ||
![]() |
bf29312ecf | ||
![]() |
a53095b29e | ||
![]() |
d5c9e6b054 | ||
![]() |
d4f29bd2af | ||
![]() |
a6661ac759 | ||
![]() |
6aa83819d1 | ||
![]() |
8ff4c8a4a5 | ||
![]() |
1d77b8dae7 | ||
![]() |
3e0062621b | ||
![]() |
b20e220910 | ||
![]() |
0259572ded | ||
![]() |
a78866069b | ||
![]() |
b8fc83577c | ||
![]() |
c21969ab4e | ||
![]() |
e02cfc4529 | ||
![]() |
ac07c63631 | ||
![]() |
1054bc995b | ||
![]() |
3560ab6387 | ||
![]() |
e50274b962 | ||
![]() |
bb1773a1a1 | ||
![]() |
941dc3b1b4 | ||
![]() |
92d08d24ab | ||
![]() |
7f6ffe0f73 | ||
![]() |
66457ca0c7 | ||
![]() |
3344f1fd88 | ||
![]() |
c4b636f80a | ||
![]() |
0ea0975bd4 | ||
![]() |
534f3a7469 | ||
![]() |
f3110ba018 | ||
![]() |
e76cc81fe1 | ||
![]() |
adcc3343ec | ||
![]() |
645e114a1f | ||
![]() |
65d86460cb | ||
![]() |
aaccd10c2a | ||
![]() |
a237bfd930 | ||
![]() |
73f64d93b1 | ||
![]() |
871db09447 | ||
![]() |
8d79103392 | ||
![]() |
fd765443e4 | ||
![]() |
63967d1558 | ||
![]() |
6b270885bf | ||
![]() |
47937d6aaa | ||
![]() |
7d2ba45620 | ||
![]() |
b270d819a8 | ||
![]() |
c50553fbf6 | ||
![]() |
a541c863be | ||
![]() |
254b482651 | ||
![]() |
d3c2cd4215 | ||
![]() |
4f7cc7dd6b | ||
![]() |
21f1f4e503 | ||
![]() |
47f2336673 | ||
![]() |
5ae93bf6d0 | ||
![]() |
caf5f10326 | ||
![]() |
e68dbcf4ee | ||
![]() |
a42e81cf8c | ||
![]() |
98a8588c1b | ||
![]() |
8630af7646 | ||
![]() |
268c5302e8 | ||
![]() |
d07d535993 | ||
![]() |
a8a75f22b2 | ||
![]() |
6143023502 | ||
![]() |
ca6aa5d4aa | ||
![]() |
bc028ed41f | ||
![]() |
911d3a9188 | ||
![]() |
b88e715fc5 | ||
![]() |
a4dfa5f281 | ||
![]() |
05fc1711b9 | ||
![]() |
c16fbb5b47 | ||
![]() |
7ca3e2b519 | ||
![]() |
8c8a0bf8eb | ||
![]() |
e85251d2e3 | ||
![]() |
73e4827249 | ||
![]() |
c37270ea08 | ||
![]() |
8cc33b46bb | ||
![]() |
700341f9cc | ||
![]() |
ca85ad5995 | ||
![]() |
1c8c36a224 | ||
![]() |
25b814e796 | ||
![]() |
e89566e04a | ||
![]() |
e946f388c0 | ||
![]() |
7616e8dab7 | ||
![]() |
2dc4fef4d3 | ||
![]() |
f2ca997195 | ||
![]() |
7445004abf | ||
![]() |
9b76abe2ed | ||
![]() |
a97205c9fc | ||
![]() |
bf3d069aad | ||
![]() |
27ea74722c | ||
![]() |
2525456d8b | ||
![]() |
9fa32df3a6 | ||
![]() |
5bdd5da13b | ||
![]() |
b8756edd29 | ||
![]() |
c93910e858 | ||
![]() |
ad4226ace7 | ||
![]() |
d71b3fe1bc | ||
![]() |
07858eecac | ||
![]() |
64ec6d0e58 | ||
![]() |
cf722427ab | ||
![]() |
1b7ff07efc | ||
![]() |
87533f4417 | ||
![]() |
9077c95cdd | ||
![]() |
22acc5ae96 | ||
![]() |
cf596a88ab | ||
![]() |
7354fa3050 | ||
![]() |
978497b287 | ||
![]() |
a52d745250 | ||
![]() |
aae71f8105 | ||
![]() |
5419b4b732 | ||
![]() |
caf5a8917c | ||
![]() |
beec00dcb3 | ||
![]() |
7565e809b0 | ||
![]() |
5c9a646bc1 | ||
![]() |
dd8ef288f7 | ||
![]() |
db8d2953cb | ||
![]() |
b75ca26db2 | ||
![]() |
948a04122a | ||
![]() |
b7c4562b85 | ||
![]() |
6d0fea1983 | ||
![]() |
1a158a919a | ||
![]() |
9a83bd4267 | ||
![]() |
7e3f516b04 | ||
![]() |
afd888e14d | ||
![]() |
76af6e975e | ||
![]() |
2017df9ec6 | ||
![]() |
aa1e83dc24 | ||
![]() |
20996b153d | ||
![]() |
b298e53fc4 | ||
![]() |
7fb382bee0 | ||
![]() |
2158772e3b | ||
![]() |
333298e6c3 | ||
![]() |
6e9deeba5b | ||
![]() |
15951509a7 | ||
![]() |
dd8b7e42d6 | ||
![]() |
2655c86be3 | ||
![]() |
c4c4d347cf | ||
![]() |
a3f7239c1b | ||
![]() |
f90c2ad74e | ||
![]() |
2907cd173b | ||
![]() |
779ee8294f | ||
![]() |
a229c9e10e | ||
![]() |
3f8d2e4242 | ||
![]() |
c1a8b0c303 | ||
![]() |
8e92c5b844 | ||
![]() |
c366fbde22 | ||
![]() |
890e866ae2 | ||
![]() |
8eb11a8957 | ||
![]() |
9cc65a386b | ||
![]() |
e4d4af1587 | ||
![]() |
b3aab5116a | ||
![]() |
7227c76538 | ||
![]() |
c2c59f4a9e | ||
![]() |
eeede318fd | ||
![]() |
3855bb4d56 | ||
![]() |
89fa682721 | ||
![]() |
cb701a7bbc | ||
![]() |
407325b8ce | ||
![]() |
161debb35a | ||
![]() |
fd5385b127 | ||
![]() |
e85e1410aa | ||
![]() |
ac068f353a | ||
![]() |
fe7c6d0d57 | ||
![]() |
abf1e4a8ac | ||
![]() |
f9dfb2d0c7 | ||
![]() |
da23740f17 | ||
![]() |
47f81251d7 | ||
![]() |
7e01eca7f5 | ||
![]() |
941b5c9e45 | ||
![]() |
207c0d612d | ||
![]() |
4035176d88 | ||
![]() |
b9f9968f84 | ||
![]() |
7a2dac8d5b | ||
![]() |
2fb8ad146f | ||
![]() |
2f4a7352d9 | ||
![]() |
c3ff030542 | ||
![]() |
aa05cd1449 | ||
![]() |
31cd33f86c | ||
![]() |
9abca204e3 | ||
![]() |
e9760c2100 | ||
![]() |
1c5bab63a5 | ||
![]() |
0d80957639 | ||
![]() |
6c73ddcaca | ||
![]() |
1bb86fe4a8 | ||
![]() |
6b7be7a82d | ||
![]() |
37b25d8422 | ||
![]() |
49edd1a6dc | ||
![]() |
a338c6e60a | ||
![]() |
f9805f3bc7 | ||
![]() |
b24c4ea030 | ||
![]() |
0cabac1eed | ||
![]() |
7c08dbfbd2 | ||
![]() |
c0ec74bbb7 | ||
![]() |
ea834f6778 | ||
![]() |
2271f32140 | ||
![]() |
93906b9b17 | ||
![]() |
40d84b7a82 | ||
![]() |
d0ee569989 | ||
![]() |
92d969b075 | ||
![]() |
49b70d0efd | ||
![]() |
26779ef1fb | ||
![]() |
fd19af23a6 | ||
![]() |
f798fef212 | ||
![]() |
c74eab4fb5 | ||
![]() |
6a0198639f | ||
![]() |
8c870f2db8 | ||
![]() |
73d287e7ee | ||
![]() |
9eb3eea3f1 | ||
![]() |
df769396b1 | ||
![]() |
c2e47ca9dc | ||
![]() |
b348159e0e | ||
![]() |
45b62f0e77 | ||
![]() |
1fb420c2fc | ||
![]() |
b4f2bc1cb3 | ||
![]() |
d6cc182da2 | ||
![]() |
9d7baa86aa | ||
![]() |
872cd90dc6 | ||
![]() |
00ab816791 | ||
![]() |
2a9e9962e8 | ||
![]() |
ed25dd931e | ||
![]() |
66031f1bc2 | ||
![]() |
a902872880 | ||
![]() |
4c2d440871 | ||
![]() |
0da17de422 | ||
![]() |
07025ae76b | ||
![]() |
d99fe944f3 | ||
![]() |
408b2a473e | ||
![]() |
fc22e9e28a | ||
![]() |
d1c44ab7b1 | ||
![]() |
4ddac50d9b | ||
![]() |
7208ad67f1 | ||
![]() |
fffdeb1320 | ||
![]() |
e8fa7d8812 | ||
![]() |
dfdb92957e | ||
![]() |
441069f04b | ||
![]() |
201995eb90 | ||
![]() |
5863319c0b | ||
![]() |
2986d85b26 | ||
![]() |
f312457f35 | ||
![]() |
de501f5ba3 | ||
![]() |
c5e5141b21 | ||
![]() |
c08cf61d0c | ||
![]() |
6728382141 | ||
![]() |
7c3f104d1b | ||
![]() |
ad6be11bbc | ||
![]() |
f09faf6645 | ||
![]() |
117a7762e1 | ||
![]() |
3d47f494a8 | ||
![]() |
e0ebdc9045 | ||
![]() |
53f8e9328d | ||
![]() |
687e0b563b | ||
![]() |
6232cc7d49 | ||
![]() |
2a6670a404 | ||
![]() |
447efc7096 | ||
![]() |
c47878202d | ||
![]() |
349076bf34 | ||
![]() |
1748bf2e2a | ||
![]() |
d9a7730511 | ||
![]() |
4a239cc217 | ||
![]() |
ce2534c5b7 | ||
![]() |
3083f5fd55 | ||
![]() |
2045066b16 | ||
![]() |
995177498f | ||
![]() |
c00b7b62d6 | ||
![]() |
93b772f197 | ||
![]() |
7782f94daa | ||
![]() |
7c97dc8004 | ||
![]() |
34ce00e2d5 | ||
![]() |
702658cca5 | ||
![]() |
e472fe0276 | ||
![]() |
fb1c381ab7 | ||
![]() |
03c7998c11 | ||
![]() |
35729fc36b | ||
![]() |
f6ce603e45 | ||
![]() |
81a75ca955 | ||
![]() |
b57c9a51f8 | ||
![]() |
df396966b0 | ||
![]() |
b8897e0193 | ||
![]() |
150e8112ea | ||
![]() |
196f16b941 | ||
![]() |
21cb7a4847 | ||
![]() |
bb2dac7504 | ||
![]() |
f5fd2f2be3 | ||
![]() |
e6ea3879c3 | ||
![]() |
c07895d418 | ||
![]() |
58fb1cf4c3 | ||
![]() |
52cc8cb8fc | ||
![]() |
1b30dab8eb | ||
![]() |
40df4a94a7 | ||
![]() |
262d06f035 | ||
![]() |
408ab99774 | ||
![]() |
28cb21db13 | ||
![]() |
7f37f4ca41 | ||
![]() |
5f85258e84 | ||
![]() |
cde1776a2d | ||
![]() |
7f6303391a | ||
![]() |
66c7806cfa | ||
![]() |
2cdb6945ba | ||
![]() |
ca45855ed7 | ||
![]() |
07ed90ed11 | ||
![]() |
c1b97b1b44 | ||
![]() |
674019ea75 | ||
![]() |
8f762484f2 | ||
![]() |
4174991345 | ||
![]() |
71064cc760 | ||
![]() |
4c40c8ff30 | ||
![]() |
d7211b130b | ||
![]() |
e40d5a0a5d | ||
![]() |
553fbf1a77 | ||
![]() |
d3a4753b79 | ||
![]() |
12cc0de571 | ||
![]() |
cbd531e161 | ||
![]() |
a8bbe02e21 | ||
![]() |
6605d5ee63 | ||
![]() |
df8bacd82e | ||
![]() |
ee831da52d | ||
![]() |
8f969374c7 | ||
![]() |
5a788b04b5 | ||
![]() |
b88a45aa79 | ||
![]() |
3c20a056e6 | ||
![]() |
82a57d34b8 | ||
![]() |
c4d7076fe8 | ||
![]() |
00b11157b2 | ||
![]() |
b63bb1ac0c | ||
![]() |
882d0ecba8 | ||
![]() |
963f1a11eb | ||
![]() |
ba8acb40ec | ||
![]() |
1d4ea2164f | ||
![]() |
0199243ce9 | ||
![]() |
4c8b97afb3 | ||
![]() |
3bac0225e5 | ||
![]() |
7d13c8d9be | ||
![]() |
4190a87171 | ||
![]() |
69e719c4c7 | ||
![]() |
c5df8742c5 | ||
![]() |
5867edcc70 | ||
![]() |
b3a7255eed | ||
![]() |
4179def119 | ||
![]() |
ef9b897394 | ||
![]() |
c225dd89c6 | ||
![]() |
d59ebad167 | ||
![]() |
347932b213 | ||
![]() |
32bc615e78 | ||
![]() |
355373f24d | ||
![]() |
a044d2fe86 | ||
![]() |
1d44eff896 | ||
![]() |
c1b746ed51 | ||
![]() |
3381ad042b | ||
![]() |
a1b5766a71 | ||
![]() |
aeeb1d4294 | ||
![]() |
7ceec47246 | ||
![]() |
ede510139f | ||
![]() |
cc848ef9f2 | ||
![]() |
1c43ee1b18 | ||
![]() |
741f540f77 | ||
![]() |
fdd26ba8f1 | ||
![]() |
bec61f4ce7 | ||
![]() |
2af1496f11 | ||
![]() |
f9e23a17d9 | ||
![]() |
8050fa58a5 | ||
![]() |
e04023bdfc | ||
![]() |
950f853fa3 | ||
![]() |
c3c15e222d | ||
![]() |
3906816e67 | ||
![]() |
fe43e21484 | ||
![]() |
5f7fd506a3 | ||
![]() |
65a3e51ff9 | ||
![]() |
722c1c6257 | ||
![]() |
2f872375ef | ||
![]() |
f119ca683e | ||
![]() |
176c6b76cd | ||
![]() |
64604dbcc6 | ||
![]() |
0344a3f104 | ||
![]() |
c17247da58 | ||
![]() |
9601daedb7 | ||
![]() |
f958f3751d | ||
![]() |
0d1b9641c7 | ||
![]() |
9a9dd43646 | ||
![]() |
222257d25d | ||
![]() |
dd961ad30b | ||
![]() |
abf2dc3efc | ||
![]() |
99f819dfbf | ||
![]() |
dc484d79ed | ||
![]() |
42f522532e | ||
![]() |
f97b439bc0 | ||
![]() |
65a4a959bb | ||
![]() |
07d6fde34e | ||
![]() |
b1dfbcbceb | ||
![]() |
5e6f7e41e6 | ||
![]() |
df95ab1217 | ||
![]() |
edf924d012 | ||
![]() |
8886732b21 | ||
![]() |
543098cba3 | ||
![]() |
8bb20de912 | ||
![]() |
7f118c9841 | ||
![]() |
73155cca66 | ||
![]() |
267ccf6f97 | ||
![]() |
1f8e09868d | ||
![]() |
c9a2a47ee1 | ||
![]() |
58f7811c7d | ||
![]() |
a32ecb032e | ||
![]() |
bc2ad581ba | ||
![]() |
3498d59258 | ||
![]() |
f8accd62ed | ||
![]() |
25b10490a1 | ||
![]() |
800f0f45e5 | ||
![]() |
57c4a285d8 | ||
![]() |
1104987b9b | ||
![]() |
3dd646485f | ||
![]() |
13eb1718aa | ||
![]() |
e41d4dad3c | ||
![]() |
9c1e32d4ba | ||
![]() |
ce71b81dbe | ||
![]() |
62cc2201b0 | ||
![]() |
831c9aee2a | ||
![]() |
cd697d7205 | ||
![]() |
4be22e9ece | ||
![]() |
482ea6d8f9 | ||
![]() |
bcf0d80c47 | ||
![]() |
9bb292f38e | ||
![]() |
1d89cf2b75 | ||
![]() |
8afc872252 | ||
![]() |
559036518c | ||
![]() |
b0562f79d2 | ||
![]() |
f57df3f2c1 | ||
![]() |
0da123265c | ||
![]() |
cf340f48c3 | ||
![]() |
f0e0eaace4 | ||
![]() |
4140d49db3 | ||
![]() |
8bd9ff58c4 | ||
![]() |
fecccb0b28 | ||
![]() |
43d79ebd05 | ||
![]() |
ad7c8760eb | ||
![]() |
22f6cc1c6f | ||
![]() |
98dcb13201 | ||
![]() |
497e8b5f28 | ||
![]() |
4108979b65 | ||
![]() |
e01aec7b52 | ||
![]() |
c5b311359a | ||
![]() |
624b95921a | ||
![]() |
69c35f7f79 | ||
![]() |
3424b996c8 | ||
![]() |
ef634227aa | ||
![]() |
c724e4cb20 | ||
![]() |
936142cf7e | ||
![]() |
2aa37571f0 | ||
![]() |
9b42960b2f | ||
![]() |
6647167d02 | ||
![]() |
835f2cf769 | ||
![]() |
3fe5d7711f | ||
![]() |
faf9b113a9 | ||
![]() |
f72cc6bbbc | ||
![]() |
69e85a7ac6 | ||
![]() |
82b65399af | ||
![]() |
a83e397643 | ||
![]() |
f02c090b8d | ||
![]() |
49dd6553fb | ||
![]() |
d116cd7e90 | ||
![]() |
ed18842281 | ||
![]() |
4a3bd5fe7a | ||
![]() |
8a70cb59d1 | ||
![]() |
35772b0370 | ||
![]() |
68b33fcfb9 | ||
![]() |
0b306219c1 | ||
![]() |
bdd05a3f71 | ||
![]() |
c11205f3cd | ||
![]() |
11a0aa322f | ||
![]() |
a12bb4ee7c | ||
![]() |
1cc5422557 | ||
![]() |
55ed4dbc51 | ||
![]() |
201b8dccaa | ||
![]() |
544cd96e3d | ||
![]() |
2604da104d | ||
![]() |
a392d3b1b4 | ||
![]() |
b7ef95f68c | ||
![]() |
d12166a872 | ||
![]() |
74d9fcdbbc | ||
![]() |
3e9536cef8 | ||
![]() |
95c2c25f1b | ||
![]() |
81932e5d9f | ||
![]() |
d91d957777 | ||
![]() |
804ac8b4dc | ||
![]() |
bde1e32e29 | ||
![]() |
cb25db2556 | ||
![]() |
1536e62734 | ||
![]() |
401c2c7cc1 | ||
![]() |
d0faf9bc2f | ||
![]() |
81b5016168 | ||
![]() |
dcbe41bc40 | ||
![]() |
4e112dc98c | ||
![]() |
d233558b44 | ||
![]() |
207c2ef5b6 | ||
![]() |
d9ccc43d15 | ||
![]() |
ee93013220 | ||
![]() |
677fc5aa68 | ||
![]() |
8a86d0e655 | ||
![]() |
27c4c0c749 | ||
![]() |
2c0cdcf947 | ||
![]() |
a56755e2c6 | ||
![]() |
765de94ca3 | ||
![]() |
f060dc896f | ||
![]() |
1dcbcef705 | ||
![]() |
5be64e124b | ||
![]() |
f70df03b1a | ||
![]() |
8b577ca12f | ||
![]() |
91719435d9 | ||
![]() |
b1b82301d9 | ||
![]() |
fdf15f53d7 | ||
![]() |
4802854cff | ||
![]() |
b0538099cf | ||
![]() |
1fa9d7d7d7 | ||
![]() |
2b66762dec | ||
![]() |
7063f254c6 | ||
![]() |
7c9f15d8a9 | ||
![]() |
2982dc478c | ||
![]() |
47fc1b7357 | ||
![]() |
6990d7632a | ||
![]() |
4fa0f990e5 | ||
![]() |
e0f789bc14 | ||
![]() |
b93dabd670 | ||
![]() |
83528df18b | ||
![]() |
1e65a09e8e | ||
![]() |
34b4f65c20 | ||
![]() |
de4f9e4257 | ||
![]() |
dc2c16d6b5 | ||
![]() |
fbb175608d | ||
![]() |
ab026b1635 | ||
![]() |
0bc09defa7 | ||
![]() |
04352494a0 | ||
![]() |
96c76177af | ||
![]() |
d5a14031c6 | ||
![]() |
ad0b5e7583 | ||
![]() |
f2424095e0 | ||
![]() |
db2bed896a | ||
![]() |
c3600ee8fc | ||
![]() |
bb5efb3450 | ||
![]() |
2e1764af82 | ||
![]() |
a0c704cd94 | ||
![]() |
76f537a636 | ||
![]() |
fdd0d781ca | ||
![]() |
47338cf07b | ||
![]() |
4c1884fc95 | ||
![]() |
c2ba053737 | ||
![]() |
4cd16a40d0 | ||
![]() |
0038c6c65a | ||
![]() |
56b0f13525 | ||
![]() |
974315868d | ||
![]() |
5634954b7e | ||
![]() |
dab1eece4c | ||
![]() |
d01b73a661 | ||
![]() |
5754b4c6af | ||
![]() |
e769ef7d0b | ||
![]() |
41a694e4a4 | ||
![]() |
76a05d2dc9 | ||
![]() |
1895601198 | ||
![]() |
f6bfdb2ced | ||
![]() |
1eb7ce8741 | ||
![]() |
a5aaf760d0 | ||
![]() |
df2ebf93b6 | ||
![]() |
4dc64ee15b | ||
![]() |
a83ae05316 | ||
![]() |
b0e6170d93 | ||
![]() |
890122894f | ||
![]() |
9c019cafd5 | ||
![]() |
b357047e3a | ||
![]() |
f932e947e0 | ||
![]() |
c21baeda7a | ||
![]() |
4d5c372832 | ||
![]() |
9b46587b73 | ||
![]() |
38ff0e39d6 | ||
![]() |
1f838b4b25 | ||
![]() |
ffbb62a280 | ||
![]() |
b0831f0b9d | ||
![]() |
e1ddbd300d | ||
![]() |
fa4d6e9ef4 | ||
![]() |
61a6f9d556 | ||
![]() |
1ae40c7b15 | ||
![]() |
45c72f0c28 | ||
![]() |
8d863638cc | ||
![]() |
759004e2b2 | ||
![]() |
36d163e464 | ||
![]() |
96dddecd4d | ||
![]() |
ece9a5666e | ||
![]() |
9b721d83dc | ||
![]() |
9c624e8e52 | ||
![]() |
a7ff5c4583 | ||
![]() |
2291321b46 | ||
![]() |
0ce2fca40a | ||
![]() |
4a6955c173 | ||
![]() |
fdfdcf915c | ||
![]() |
222fd9de62 | ||
![]() |
1093881149 | ||
![]() |
cb876436d4 | ||
![]() |
13758c9568 | ||
![]() |
52af3e8aa8 | ||
![]() |
aeb850f4fb | ||
![]() |
3249af4eaa | ||
![]() |
737b3be5be | ||
![]() |
3537848d3d | ||
![]() |
5387bf19d9 | ||
![]() |
357f2183b5 | ||
![]() |
c586d9f54c | ||
![]() |
abfa44a088 | ||
![]() |
5517beff80 | ||
![]() |
74c0c13fbd | ||
![]() |
b086e4c2a1 | ||
![]() |
b45306487b | ||
![]() |
f2c9404fc4 | ||
![]() |
b703a6f5fb | ||
![]() |
f8c3faec79 | ||
![]() |
5432363aa1 | ||
![]() |
85c8cd6340 | ||
![]() |
4c00a25968 | ||
![]() |
b74347d21f | ||
![]() |
657142716c | ||
![]() |
618440e38f | ||
![]() |
19b7ce0668 | ||
![]() |
39401b3bfa | ||
![]() |
f22938030d | ||
![]() |
a70935586b | ||
![]() |
b1abc79942 | ||
![]() |
69c3d61c28 | ||
![]() |
4e891151f4 | ||
![]() |
8e79e9e459 | ||
![]() |
f9085667d1 | ||
![]() |
a35ac7613e | ||
![]() |
ecedff2cdf | ||
![]() |
500af07689 | ||
![]() |
6779e15872 | ||
![]() |
b3b52fce62 | ||
![]() |
0e02998faf | ||
![]() |
19275a5ba4 | ||
![]() |
701893b472 | ||
![]() |
66cb7293e7 | ||
![]() |
773ddb6e9e | ||
![]() |
7ebff68506 | ||
![]() |
9e75547166 | ||
![]() |
bb4e6c74e0 | ||
![]() |
55d043a626 | ||
![]() |
5046af5313 | ||
![]() |
5e77958106 | ||
![]() |
63528ce8f3 | ||
![]() |
856f53067b | ||
![]() |
58a89ad182 | ||
![]() |
e815d97265 | ||
![]() |
33db722f68 | ||
![]() |
0058919a8e | ||
![]() |
5771c0f56e | ||
![]() |
8b13a23117 | ||
![]() |
08b6397a9a | ||
![]() |
66441e28cb | ||
![]() |
26b521411f | ||
![]() |
03b252024e | ||
![]() |
68f3f695cd | ||
![]() |
c0d25786ef | ||
![]() |
8caf03327c | ||
![]() |
9815f9a5a1 | ||
![]() |
f2f5955264 | ||
![]() |
e5d69465ab | ||
![]() |
eba06a2139 | ||
![]() |
daa847d29b | ||
![]() |
433b2734bb | ||
![]() |
02f7a5f55b | ||
![]() |
2556807166 | ||
![]() |
5bf6633cbe | ||
![]() |
acb4271833 | ||
![]() |
dd0bb43616 | ||
![]() |
a11a22453a | ||
![]() |
39e6207183 | ||
![]() |
88ef235987 | ||
![]() |
32399ba8a1 | ||
![]() |
42052dde9f | ||
![]() |
ea686ee8e2 | ||
![]() |
df05d4cc08 | ||
![]() |
e54a8ae92b | ||
![]() |
0f8043b35b | ||
![]() |
ec1f3665eb | ||
![]() |
444072db13 | ||
![]() |
6acfcfd23c | ||
![]() |
78fdc5b07e | ||
![]() |
f5fcdf4acb | ||
![]() |
8aa58a722b | ||
![]() |
f7b0291bf8 | ||
![]() |
179597b86f | ||
![]() |
f6a7b2add6 | ||
![]() |
3147a93ca6 | ||
![]() |
cb7117a36b | ||
![]() |
b5912eb9f6 | ||
![]() |
a3c54f22c8 | ||
![]() |
70edfa384b | ||
![]() |
077eb5b079 | ||
![]() |
e654311997 | ||
![]() |
0d0ddd549b | ||
![]() |
ce466bd6ad | ||
![]() |
f67f25bcdc | ||
![]() |
488f281ec7 | ||
![]() |
7b791d622f | ||
![]() |
f3c8ec496a | ||
![]() |
9f4712f1f8 | ||
![]() |
f9d7dd2977 | ||
![]() |
63d030d4c9 | ||
![]() |
7b308b0e50 | ||
![]() |
5e13be92f5 | ||
![]() |
325fc22a3b | ||
![]() |
5196ac8d32 | ||
![]() |
79a7a03d03 | ||
![]() |
b97d7080e5 | ||
![]() |
b8709d10c7 | ||
![]() |
762d570f8b | ||
![]() |
a2f1ddddb5 | ||
![]() |
b5b98cf392 | ||
![]() |
4827267af6 | ||
![]() |
de63d534c5 | ||
![]() |
269aafd625 | ||
![]() |
d8e31665a0 | ||
![]() |
5c19b70e83 | ||
![]() |
064c741e3f | ||
![]() |
f05b28218c | ||
![]() |
4b4e6ea035 | ||
![]() |
861b8a4450 | ||
![]() |
70ad86534d | ||
![]() |
7714e8b50a | ||
![]() |
a63b6bf18c | ||
![]() |
70c79f6127 | ||
![]() |
de62b2e65c | ||
![]() |
1103492193 | ||
![]() |
fc3eeff1c9 | ||
![]() |
205711da7e | ||
![]() |
57709942a0 | ||
![]() |
5e582ceb98 | ||
![]() |
25d50fed74 | ||
![]() |
07ac2ecf4c | ||
![]() |
7908c39ef4 | ||
![]() |
b64ef705e8 | ||
![]() |
36664fb251 | ||
![]() |
fa8f8151f6 | ||
![]() |
bebedf1a50 | ||
![]() |
f53a0af150 | ||
![]() |
09bf430a1e | ||
![]() |
b221914a3d | ||
![]() |
1d66794450 | ||
![]() |
1563a2392d | ||
![]() |
a19b0e028f | ||
![]() |
fa60a746a2 | ||
![]() |
227533a7f9 | ||
![]() |
4122e0bf1d | ||
![]() |
3a42331875 | ||
![]() |
37b7ea3b0a | ||
![]() |
a772877ae1 | ||
![]() |
76b6bdfd06 | ||
![]() |
2a8e91190c | ||
![]() |
be8a638fce | ||
![]() |
3e2da6a386 | ||
![]() |
ff547035be | ||
![]() |
ca9136d1cb | ||
![]() |
439be1c222 | ||
![]() |
77c60b91c6 | ||
![]() |
a5b1a92920 | ||
![]() |
58fa59e06d | ||
![]() |
fc6c5bf585 | ||
![]() |
e52ef501bb | ||
![]() |
6ffba92dc8 | ||
![]() |
3ade1ea59b | ||
![]() |
427a791de5 | ||
![]() |
dbe6ea68d5 | ||
![]() |
b6c6eb4dfa | ||
![]() |
fdceaadec5 | ||
![]() |
4dc56f4678 | ||
![]() |
1b79d50288 | ||
![]() |
1b695a49e9 | ||
![]() |
fd109b5770 | ||
![]() |
80b588683e | ||
![]() |
b18fa1f13f | ||
![]() |
f1ddf07a45 | ||
![]() |
5f5a4b61b0 | ||
![]() |
20ca03496f | ||
![]() |
7af66c4911 | ||
![]() |
2e3a2f045e | ||
![]() |
c2563c2f87 | ||
![]() |
53d8118b8f | ||
![]() |
9bd987f7f3 | ||
![]() |
a370e9d4cb | ||
![]() |
65210e4cbc | ||
![]() |
2008c50ebf | ||
![]() |
3ab989cf3c | ||
![]() |
d8e9cb93b7 | ||
![]() |
cd4cff7e43 | ||
![]() |
9b735778c2 | ||
![]() |
a7ac28b717 | ||
![]() |
4f87079f12 | ||
![]() |
840388e8c5 | ||
![]() |
5297ddf68c | ||
![]() |
4f4c606949 | ||
![]() |
dbc8aa159d | ||
![]() |
103a048e4d | ||
![]() |
a8f6275763 | ||
![]() |
0f600c3cc2 | ||
![]() |
9c1f9a54b1 | ||
![]() |
2f605497be | ||
![]() |
07adafe6f3 | ||
![]() |
15b178a158 | ||
![]() |
22be831535 | ||
![]() |
71ebe047c6 | ||
![]() |
f350d28dbd | ||
![]() |
a7d713c323 | ||
![]() |
ef739fb222 | ||
![]() |
cff445b64b | ||
![]() |
b16f9cfdf7 | ||
![]() |
b12c952a6a | ||
![]() |
9bab3054e9 | ||
![]() |
927a0aba06 | ||
![]() |
8811bbed09 | ||
![]() |
31a2389d04 | ||
![]() |
a3c13e75cf | ||
![]() |
6037eeba00 | ||
![]() |
ce50364b9f | ||
![]() |
14b04413b5 | ||
![]() |
dc2212a57f | ||
![]() |
fe40e7bdf1 | ||
![]() |
ee3c146785 | ||
![]() |
296a554637 | ||
![]() |
b56a39e576 | ||
![]() |
7898358617 | ||
![]() |
9a3900debb | ||
![]() |
dcb152aa00 | ||
![]() |
2e50ad802f | ||
![]() |
1e169315fd | ||
![]() |
3df6cd07d0 | ||
![]() |
8e681b5534 | ||
![]() |
a12abc2a6c | ||
![]() |
705e273400 | ||
![]() |
78f36dfd16 | ||
![]() |
30ed217c79 | ||
![]() |
a74e6b53cd | ||
![]() |
57e29a5725 | ||
![]() |
369cfa7e88 | ||
![]() |
1048150654 | ||
![]() |
697c86362c | ||
![]() |
d8b48e6373 | ||
![]() |
b04d7ad8f2 | ||
![]() |
05692b9947 | ||
![]() |
3203eb5eda | ||
![]() |
2c26b4c6ac | ||
![]() |
5c33abca21 | ||
![]() |
b7f871607e | ||
![]() |
babe12cd7b | ||
![]() |
bb4d352da0 | ||
![]() |
9c055fb165 | ||
![]() |
b02da2d0cd | ||
![]() |
69a982bf8d | ||
![]() |
66547c5abd | ||
![]() |
159cc24e78 | ||
![]() |
777ad14762 | ||
![]() |
78705165f0 | ||
![]() |
d521fbbd2c | ||
![]() |
b6aa5ada30 | ||
![]() |
d76a6dff89 | ||
![]() |
ecb8dd29b7 | ||
![]() |
bfd2f2bf57 | ||
![]() |
7ddc5d525c | ||
![]() |
efa7f986a4 | ||
![]() |
df74a2763c | ||
![]() |
60754250d9 | ||
![]() |
9055b4b773 | ||
![]() |
91784d6380 | ||
![]() |
63fdb18286 | ||
![]() |
b75dfd3ece | ||
![]() |
9881364e1d | ||
![]() |
4f5179f5c9 | ||
![]() |
79d5f0e90c | ||
![]() |
09d2a9d897 | ||
![]() |
0c79c492e2 | ||
![]() |
becca2d05e | ||
![]() |
1cf4cdcee9 | ||
![]() |
c9e410fcb1 | ||
![]() |
59edd881e2 | ||
![]() |
d3b35742a6 | ||
![]() |
9acebda035 | ||
![]() |
23a5e53d50 | ||
![]() |
30c531417a | ||
![]() |
bc01e151c4 | ||
![]() |
62ca0e5b09 | ||
![]() |
0696833ad6 | ||
![]() |
0668f424e1 | ||
![]() |
1fe87d8883 | ||
![]() |
9e7e5de63a | ||
![]() |
78ab59a55e | ||
![]() |
4023fe5b59 | ||
![]() |
dafa1f3ede | ||
![]() |
cf99a5d598 | ||
![]() |
b06d39726f | ||
![]() |
0f8ad15e45 | ||
![]() |
8057041bc7 | ||
![]() |
ebf9439327 | ||
![]() |
c0a91c0aba | ||
![]() |
0aefa56e1d | ||
![]() |
14a28fedcc | ||
![]() |
fe91be11e8 | ||
![]() |
b93fd86092 | ||
![]() |
880ca257a6 | ||
![]() |
96f586900c | ||
![]() |
c3a41b172e | ||
![]() |
586a6950b2 | ||
![]() |
2ec2ceea14 | ||
![]() |
24fe1a6c89 | ||
![]() |
31aa73ce07 | ||
![]() |
6c930e2d8d | ||
![]() |
847b41f49a | ||
![]() |
e4a47611c0 | ||
![]() |
0b500d2585 | ||
![]() |
ebd2d85de7 | ||
![]() |
c5a9a1fca4 | ||
![]() |
e66d805672 | ||
![]() |
468312b6cc | ||
![]() |
8edc41dee0 | ||
![]() |
5535111314 | ||
![]() |
0890b1e369 | ||
![]() |
64c8eb594b | ||
![]() |
55021376b5 | ||
![]() |
3f7ec8a148 | ||
![]() |
5f9b46b98b | ||
![]() |
a1d8f28fc2 | ||
![]() |
5ace384607 | ||
![]() |
588c94a64f | ||
![]() |
7fc10b71e3 | ||
![]() |
57045ef1f6 | ||
![]() |
36822b1f91 | ||
![]() |
68f87435de | ||
![]() |
6deec4bbb5 | ||
![]() |
cf271100f5 | ||
![]() |
2af129d0b7 | ||
![]() |
85e1bdb650 | ||
![]() |
c418bc6512 | ||
![]() |
44094680fb | ||
![]() |
fc46958ac0 | ||
![]() |
53bc5a51e9 | ||
![]() |
f87a690df1 | ||
![]() |
91a1c3d107 | ||
![]() |
1d14f8ad08 | ||
![]() |
1b68e9394b | ||
![]() |
b28c345ba0 | ||
![]() |
07dad2a0ef | ||
![]() |
19dd2834fb | ||
![]() |
f15587807a | ||
![]() |
3cdd925c41 | ||
![]() |
02da446914 | ||
![]() |
810ced3907 | ||
![]() |
b3b928ae4f | ||
![]() |
2411a7677d | ||
![]() |
ff495a45a8 | ||
![]() |
07b6dd247d | ||
![]() |
4cd8776d06 | ||
![]() |
7fe503c89a | ||
![]() |
76aa05f320 | ||
![]() |
802d9abb1d | ||
![]() |
63901ccf77 | ||
![]() |
e16234e3e8 | ||
![]() |
71d5fad2b0 | ||
![]() |
c5a711af0f | ||
![]() |
997133c979 | ||
![]() |
4a3a123f42 | ||
![]() |
d5ec71c5da | ||
![]() |
8fff29224d | ||
![]() |
63c7eed2a9 | ||
![]() |
4f790afe49 | ||
![]() |
8a12e9cf94 | ||
![]() |
bc2512ac5e | ||
![]() |
4f66d6713f | ||
![]() |
06be2e9dfb | ||
![]() |
55dc17d9a7 | ||
![]() |
4616f4bfd9 | ||
![]() |
85cac531c9 | ||
![]() |
af921a0cb3 | ||
![]() |
4f7d160513 | ||
![]() |
0e8dee0506 | ||
![]() |
e7f0613d82 | ||
![]() |
3c1882d2b4 | ||
![]() |
78a3206295 | ||
![]() |
57952f6f55 | ||
![]() |
396145d625 | ||
![]() |
da85aa31aa | ||
![]() |
63dda89035 | ||
![]() |
724d47fb6a | ||
![]() |
180802b985 | ||
![]() |
68d50ba2f5 | ||
![]() |
cbc9dfecf6 | ||
![]() |
f50feba78d | ||
![]() |
3d68688f33 | ||
![]() |
198ddb7433 | ||
![]() |
d028ed9f6c | ||
![]() |
f595f4ad04 | ||
![]() |
7033f7f7c4 | ||
![]() |
f73bcb78a8 | ||
![]() |
1731e77a25 | ||
![]() |
003d937a6e | ||
![]() |
063fde1a02 | ||
![]() |
61f7c85060 | ||
![]() |
a7da396a4c | ||
![]() |
fbfd6f0b47 | ||
![]() |
799ebc6aa0 | ||
![]() |
5a01f4854c | ||
![]() |
4417d94913 | ||
![]() |
9f3798978d | ||
![]() |
254da05964 | ||
![]() |
58f30494b4 | ||
![]() |
abb6139f32 | ||
![]() |
0873b1d161 | ||
![]() |
c9702c3a6d | ||
![]() |
66c0cb6e17 | ||
![]() |
fe20f5061f | ||
![]() |
2342831104 | ||
![]() |
ee2bbef9da | ||
![]() |
34c85eb150 | ||
![]() |
33e044806b | ||
![]() |
465a0e9e04 | ||
![]() |
e4a9a03239 | ||
![]() |
0c7e1feb4b | ||
![]() |
0a9b67976c | ||
![]() |
a624b24bfc | ||
![]() |
d44868ecab | ||
![]() |
df8611df11 | ||
![]() |
6c8bc117ab | ||
![]() |
c93f528f96 | ||
![]() |
10c5e57b98 | ||
![]() |
c2a27301b7 | ||
![]() |
98284bd5e2 | ||
![]() |
1a00430999 | ||
![]() |
dfd5f70013 | ||
![]() |
4172291a10 | ||
![]() |
33a4ed8c95 | ||
![]() |
4604e89b4f | ||
![]() |
311dcd27a8 | ||
![]() |
8d71307831 | ||
![]() |
2ebef51d38 | ||
![]() |
d9bd509d22 |
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!requirements.txt
|
@@ -9,5 +9,11 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.ts]
|
||||
indent_style = tab
|
||||
|
444
.eslintrc.yml
@@ -4,103 +4,73 @@ env:
|
||||
es6: true
|
||||
node: true
|
||||
mocha: true
|
||||
extends: 'eslint:recommended'
|
||||
plugins:
|
||||
- lodash
|
||||
- jsdoc
|
||||
- node
|
||||
- react
|
||||
extends: 'standard'
|
||||
parserOptions:
|
||||
sourceType: 'script'
|
||||
ecmaFeatures:
|
||||
jsx: true
|
||||
settings:
|
||||
jsdoc:
|
||||
additionalTagNames:
|
||||
customTags:
|
||||
- fulfil
|
||||
rules:
|
||||
|
||||
# Possible Errors
|
||||
|
||||
comma-dangle:
|
||||
- error
|
||||
- never
|
||||
no-cond-assign:
|
||||
- error
|
||||
no-console:
|
||||
- off
|
||||
no-constant-condition:
|
||||
- error
|
||||
no-control-regex:
|
||||
- error
|
||||
no-debugger:
|
||||
- error
|
||||
no-dupe-args:
|
||||
- error
|
||||
no-dupe-keys:
|
||||
- error
|
||||
no-duplicate-case:
|
||||
- error
|
||||
no-empty:
|
||||
- error
|
||||
no-empty-character-class:
|
||||
- error
|
||||
no-ex-assign:
|
||||
- error
|
||||
no-extra-boolean-cast:
|
||||
- error
|
||||
no-extra-parens:
|
||||
- error
|
||||
no-extra-semi:
|
||||
- error
|
||||
no-func-assign:
|
||||
- error
|
||||
no-inner-declarations:
|
||||
- error
|
||||
- both
|
||||
no-invalid-regexp:
|
||||
- error
|
||||
no-irregular-whitespace:
|
||||
- error
|
||||
no-negated-in-lhs:
|
||||
- error
|
||||
no-obj-calls:
|
||||
- error
|
||||
no-prototype-builtins:
|
||||
- error
|
||||
no-regex-spaces:
|
||||
- error
|
||||
no-sparse-arrays:
|
||||
- error
|
||||
no-unexpected-multiline:
|
||||
- error
|
||||
no-unreachable:
|
||||
- error
|
||||
no-unsafe-finally:
|
||||
- error
|
||||
use-isnan:
|
||||
- error
|
||||
valid-jsdoc:
|
||||
- error
|
||||
- requireReturn: false
|
||||
requireReturnDescription: false
|
||||
valid-typeof:
|
||||
- error
|
||||
requireReturnType: true
|
||||
requireParamDescription: true
|
||||
preferType:
|
||||
boolean: "Boolean"
|
||||
number: "Number"
|
||||
object: "Object"
|
||||
string: "String"
|
||||
array: "Array"
|
||||
prefer:
|
||||
arg: "param"
|
||||
return: "returns"
|
||||
|
||||
# Best Practices
|
||||
|
||||
accessor-pairs:
|
||||
- error
|
||||
array-callback-return:
|
||||
- error
|
||||
block-scoped-var:
|
||||
- error
|
||||
class-methods-use-this:
|
||||
- error
|
||||
complexity:
|
||||
- off
|
||||
consistent-return:
|
||||
- error
|
||||
curly:
|
||||
- error
|
||||
default-case:
|
||||
- error
|
||||
dot-location:
|
||||
- error
|
||||
- property
|
||||
dot-notation:
|
||||
- error
|
||||
eqeqeq:
|
||||
- error
|
||||
guard-for-in:
|
||||
- error
|
||||
no-alert:
|
||||
- error
|
||||
no-caller:
|
||||
- error
|
||||
no-case-declarations:
|
||||
- error
|
||||
no-div-regex:
|
||||
@@ -109,93 +79,43 @@ rules:
|
||||
- error
|
||||
no-empty-function:
|
||||
- error
|
||||
no-empty-pattern:
|
||||
- error
|
||||
no-eq-null:
|
||||
- error
|
||||
no-eval:
|
||||
- error
|
||||
no-extend-native:
|
||||
- error
|
||||
no-extra-bind:
|
||||
- error
|
||||
no-extra-label:
|
||||
- error
|
||||
no-fallthrough:
|
||||
- error
|
||||
no-floating-decimal:
|
||||
- error
|
||||
no-implicit-coercion:
|
||||
- error
|
||||
no-implicit-globals:
|
||||
- error
|
||||
no-implied-eval:
|
||||
- error
|
||||
no-iterator:
|
||||
- error
|
||||
no-labels:
|
||||
- error
|
||||
no-lone-blocks:
|
||||
- error
|
||||
no-loop-func:
|
||||
- error
|
||||
no-multi-spaces:
|
||||
- error
|
||||
no-multi-str:
|
||||
no-magic-numbers:
|
||||
- error
|
||||
no-native-reassign:
|
||||
- error
|
||||
no-new:
|
||||
no-param-reassign:
|
||||
- error
|
||||
no-new-func:
|
||||
no-restricted-properties:
|
||||
- error
|
||||
no-new-wrappers:
|
||||
- error
|
||||
no-octal:
|
||||
- error
|
||||
no-octal-escape:
|
||||
- error
|
||||
no-proto:
|
||||
- error
|
||||
no-redeclare:
|
||||
- error
|
||||
no-return-assign:
|
||||
- property: __proto__
|
||||
no-return-await:
|
||||
- error
|
||||
no-script-url:
|
||||
- error
|
||||
no-self-assign:
|
||||
- error
|
||||
no-self-compare:
|
||||
- error
|
||||
no-sequences:
|
||||
- error
|
||||
no-throw-literal:
|
||||
- error
|
||||
no-unmodified-loop-condition:
|
||||
no-unused-expressions:
|
||||
- error
|
||||
no-unused-labels:
|
||||
- error
|
||||
no-useless-call:
|
||||
- error
|
||||
no-useless-concat:
|
||||
- error
|
||||
no-useless-escape:
|
||||
- error
|
||||
no-void:
|
||||
- error
|
||||
no-warning-comments:
|
||||
- off
|
||||
no-with:
|
||||
- error
|
||||
radix:
|
||||
- error
|
||||
vars-on-top:
|
||||
- off
|
||||
wrap-iife:
|
||||
- error
|
||||
- outside
|
||||
yoda:
|
||||
- error
|
||||
|
||||
# Strict mode
|
||||
|
||||
@@ -205,19 +125,17 @@ rules:
|
||||
|
||||
# Variables
|
||||
|
||||
init-declarations:
|
||||
- error
|
||||
- always
|
||||
no-catch-shadow:
|
||||
- error
|
||||
no-delete-var:
|
||||
- error
|
||||
no-label-var:
|
||||
no-restricted-globals:
|
||||
- error
|
||||
- event
|
||||
no-shadow:
|
||||
- error
|
||||
no-shadow-restricted-names:
|
||||
- error
|
||||
no-undef:
|
||||
- error
|
||||
no-undef-init:
|
||||
no-undefined:
|
||||
- error
|
||||
no-unused-vars:
|
||||
- error
|
||||
@@ -230,14 +148,8 @@ rules:
|
||||
- error
|
||||
global-require:
|
||||
- off
|
||||
handle-callback-err:
|
||||
- error
|
||||
no-mixed-requires:
|
||||
- error
|
||||
no-new-require:
|
||||
- error
|
||||
no-path-concat:
|
||||
- error
|
||||
no-process-env:
|
||||
- off
|
||||
no-process-exit:
|
||||
@@ -250,28 +162,23 @@ rules:
|
||||
array-bracket-spacing:
|
||||
- error
|
||||
- always
|
||||
block-spacing:
|
||||
- error
|
||||
brace-style:
|
||||
- error
|
||||
- 1tbs
|
||||
camelcase:
|
||||
capitalized-comments:
|
||||
- error
|
||||
- always
|
||||
- ignoreConsecutiveComments: true
|
||||
comma-spacing:
|
||||
- error
|
||||
- before: false
|
||||
after: true
|
||||
comma-style:
|
||||
- error
|
||||
- last
|
||||
computed-property-spacing:
|
||||
- error
|
||||
- never
|
||||
consistent-this:
|
||||
- error
|
||||
- self
|
||||
eol-last:
|
||||
func-name-matching:
|
||||
- error
|
||||
- always
|
||||
func-names:
|
||||
- error
|
||||
- never
|
||||
@@ -280,19 +187,20 @@ rules:
|
||||
- expression
|
||||
id-blacklist:
|
||||
- error
|
||||
indent:
|
||||
id-length:
|
||||
- error
|
||||
- 2
|
||||
- SwitchCase: 1
|
||||
key-spacing:
|
||||
- min: 2
|
||||
exceptions:
|
||||
- "_"
|
||||
id-match:
|
||||
- error
|
||||
- beforeColon: false
|
||||
afterColon: true
|
||||
mode: strict
|
||||
keyword-spacing:
|
||||
- "^[_0-9A-Za-z\\$]+$"
|
||||
line-comment-position:
|
||||
- error
|
||||
- before: true
|
||||
after: true
|
||||
- position: above
|
||||
linebreak-style:
|
||||
- error
|
||||
- unix
|
||||
lines-around-comment:
|
||||
- error
|
||||
- beforeBlockComment: true
|
||||
@@ -305,6 +213,9 @@ rules:
|
||||
allowObjectEnd: false
|
||||
allowArrayStart: true
|
||||
allowArrayEnd: false
|
||||
lines-around-directive:
|
||||
- error
|
||||
- always
|
||||
max-len:
|
||||
- error
|
||||
- code: 130
|
||||
@@ -312,96 +223,74 @@ rules:
|
||||
ignoreComments: false
|
||||
ignoreTrailingComments: false
|
||||
ignoreUrls: true
|
||||
max-params:
|
||||
- off
|
||||
max-statements-per-line:
|
||||
- error
|
||||
- max: 1
|
||||
new-cap:
|
||||
- error
|
||||
new-parens:
|
||||
- error
|
||||
no-array-constructor:
|
||||
- error
|
||||
multiline-ternary:
|
||||
- off
|
||||
newline-per-chained-call:
|
||||
- off
|
||||
no-bitwise:
|
||||
- error
|
||||
no-continue:
|
||||
- error
|
||||
no-inline-comments:
|
||||
- error
|
||||
no-lonely-if:
|
||||
- error
|
||||
no-mixed-operators:
|
||||
- error
|
||||
no-mixed-spaces-and-tabs:
|
||||
no-multi-assign:
|
||||
- error
|
||||
no-multiple-empty-lines:
|
||||
- error
|
||||
- max: 1
|
||||
maxEOF: 1
|
||||
maxBOF: 0
|
||||
no-negated-condition:
|
||||
- error
|
||||
no-nested-ternary:
|
||||
- error
|
||||
no-new-object:
|
||||
- error
|
||||
no-plusplus:
|
||||
- error
|
||||
no-spaced-func:
|
||||
no-restricted-syntax:
|
||||
- error
|
||||
no-trailing-spaces:
|
||||
- WithStatement
|
||||
- ForInStatement
|
||||
no-spaced-func:
|
||||
- error
|
||||
no-underscore-dangle:
|
||||
- error
|
||||
- allowAfterThis: false
|
||||
no-unneeded-ternary:
|
||||
- error
|
||||
no-whitespace-before-property:
|
||||
- error
|
||||
object-curly-newline:
|
||||
- error
|
||||
- minProperties: 1
|
||||
- minProperties: 3
|
||||
consistent: true
|
||||
object-curly-spacing:
|
||||
- error
|
||||
- always
|
||||
object-property-newline:
|
||||
one-var-declaration-per-line:
|
||||
- error
|
||||
one-var:
|
||||
- error
|
||||
- never
|
||||
- always
|
||||
operator-assignment:
|
||||
- error
|
||||
- always
|
||||
operator-linebreak:
|
||||
- error
|
||||
- before
|
||||
quote-props:
|
||||
- error
|
||||
- as-needed
|
||||
quotes:
|
||||
- error
|
||||
- single
|
||||
quote-props:
|
||||
- error
|
||||
- as-needed
|
||||
require-jsdoc:
|
||||
- error
|
||||
- require:
|
||||
FunctionDeclaration: true
|
||||
ClassDeclaration: true
|
||||
MethodDefinition: true
|
||||
semi:
|
||||
- error
|
||||
- always
|
||||
semi-spacing:
|
||||
- error
|
||||
- before: false
|
||||
after: true
|
||||
space-before-blocks:
|
||||
- error
|
||||
ArrowFunctionExpression: true
|
||||
space-before-function-paren:
|
||||
- error
|
||||
- never
|
||||
space-in-parens:
|
||||
- error
|
||||
- never
|
||||
space-infix-ops:
|
||||
- error
|
||||
spaced-comment:
|
||||
- anonymous: always
|
||||
named: always
|
||||
asyncArrow: always
|
||||
template-tag-spacing:
|
||||
- error
|
||||
- always
|
||||
unicode-bom:
|
||||
@@ -409,9 +298,6 @@ rules:
|
||||
|
||||
# ECMAScript 6
|
||||
|
||||
arrow-body-style:
|
||||
- error
|
||||
- always
|
||||
arrow-parens:
|
||||
- error
|
||||
- always
|
||||
@@ -419,48 +305,150 @@ rules:
|
||||
- error
|
||||
- before: true
|
||||
after: true
|
||||
constructor-super:
|
||||
- error
|
||||
generator-star-spacing:
|
||||
- error
|
||||
- before: true
|
||||
after: false
|
||||
no-class-assign:
|
||||
- error
|
||||
no-confusing-arrow:
|
||||
- error
|
||||
no-const-assign:
|
||||
- error
|
||||
no-dupe-class-members:
|
||||
- error
|
||||
no-duplicate-imports:
|
||||
- error
|
||||
no-new-symbol:
|
||||
- error
|
||||
no-this-before-super:
|
||||
- error
|
||||
no-useless-computed-key:
|
||||
- error
|
||||
no-useless-constructor:
|
||||
- error
|
||||
no-useless-rename:
|
||||
- error
|
||||
no-var:
|
||||
- error
|
||||
prefer-const:
|
||||
object-shorthand:
|
||||
- error
|
||||
prefer-reflect:
|
||||
- always
|
||||
prefer-const:
|
||||
- error
|
||||
prefer-spread:
|
||||
- error
|
||||
prefer-numeric-literals:
|
||||
- error
|
||||
prefer-rest-params:
|
||||
- error
|
||||
prefer-template:
|
||||
- error
|
||||
prefer-arrow-callback:
|
||||
- error
|
||||
- allowNamedFunctions: false
|
||||
require-yield:
|
||||
- error
|
||||
rest-spread-spacing:
|
||||
symbol-description:
|
||||
- error
|
||||
template-curly-spacing:
|
||||
|
||||
# Lodash
|
||||
|
||||
lodash/chain-style:
|
||||
- error
|
||||
- never
|
||||
yield-star-spacing:
|
||||
- explicit
|
||||
lodash/identity-shorthand:
|
||||
- error
|
||||
- before: true
|
||||
after: false
|
||||
- always
|
||||
lodash/import-scope:
|
||||
- error
|
||||
- full
|
||||
lodash/matches-prop-shorthand:
|
||||
- error
|
||||
- always
|
||||
lodash/matches-shorthand:
|
||||
- error
|
||||
- always
|
||||
lodash/no-commit:
|
||||
- error
|
||||
lodash/path-style:
|
||||
- error
|
||||
- array
|
||||
lodash/prefer-compact:
|
||||
- error
|
||||
lodash/prefer-filter:
|
||||
- error
|
||||
- 5
|
||||
lodash/prefer-flat-map:
|
||||
- error
|
||||
lodash/prefer-invoke-map:
|
||||
- error
|
||||
lodash/prefer-map:
|
||||
- error
|
||||
lodash/prefer-reject:
|
||||
- error
|
||||
lodash/prefer-thru:
|
||||
- error
|
||||
lodash/prefer-wrapper-method:
|
||||
- error
|
||||
lodash/prop-shorthand:
|
||||
- error
|
||||
- always
|
||||
lodash/prefer-constant:
|
||||
- error
|
||||
- true
|
||||
- true
|
||||
lodash/prefer-get:
|
||||
- error
|
||||
- 2
|
||||
lodash/prefer-includes:
|
||||
- error
|
||||
- includeNative: true
|
||||
lodash/prefer-is-nil:
|
||||
- error
|
||||
lodash/prefer-lodash-chain:
|
||||
- error
|
||||
lodash/prefer-lodash-method:
|
||||
- error
|
||||
lodash/prefer-lodash-typecheck:
|
||||
- error
|
||||
lodash/prefer-matches:
|
||||
- error
|
||||
- 3
|
||||
lodash/prefer-noop:
|
||||
- error
|
||||
lodash/prefer-over-quantifier:
|
||||
- error
|
||||
lodash/prefer-startswith:
|
||||
- error
|
||||
lodash/prefer-times:
|
||||
- error
|
||||
|
||||
# JSDoc
|
||||
|
||||
jsdoc/check-param-names:
|
||||
- error
|
||||
jsdoc/check-tag-names:
|
||||
- error
|
||||
jsdoc/newline-after-description:
|
||||
- error
|
||||
jsdoc/require-example:
|
||||
- error
|
||||
jsdoc/require-hyphen-before-param-description:
|
||||
- error
|
||||
jsdoc/require-param:
|
||||
- error
|
||||
jsdoc/require-param-description:
|
||||
- error
|
||||
jsdoc/require-param-type:
|
||||
- error
|
||||
jsdoc/require-returns-type:
|
||||
- error
|
||||
|
||||
# Node
|
||||
|
||||
node/no-deprecated-api:
|
||||
- error
|
||||
node/no-missing-import:
|
||||
- error
|
||||
node/no-missing-require:
|
||||
- error
|
||||
node/process-exit-as-throw:
|
||||
- error
|
||||
node/no-extraneous-require:
|
||||
- error
|
||||
node/no-extraneous-import:
|
||||
- error
|
||||
|
||||
# React
|
||||
|
||||
react/jsx-uses-vars:
|
||||
- error
|
||||
|
||||
overrides:
|
||||
files: ['*.jsx']
|
||||
rules:
|
||||
require-jsdoc:
|
||||
- off
|
||||
|
58
.gitattributes
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||
*.ts text eol=lf
|
||||
*.js text eol=lf
|
||||
*.jsx 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
|
||||
|
||||
# Text files
|
||||
Dockerfile* text
|
||||
.dockerignore text
|
||||
.editorconfig text
|
||||
etcher text
|
||||
.git* text
|
||||
*.html text
|
||||
*.json text eol=lf
|
||||
*.cpp text
|
||||
*.h text
|
||||
*.gyp text
|
||||
LICENSE text
|
||||
Makefile text
|
||||
*.md text
|
||||
*.sh text
|
||||
*.bat text
|
||||
*.svg text
|
||||
*.yml text
|
||||
*.patch text
|
||||
*.txt text
|
||||
CODEOWNERS text
|
||||
|
||||
# Binary files (no line-ending conversions)
|
||||
*.bz2 binary diff=hex
|
||||
*.gz binary diff=hex
|
||||
*.icns binary diff=hex
|
||||
*.ico binary diff=hex
|
||||
*.tiff binary diff=hex
|
||||
*.img binary diff=hex
|
||||
*.iso binary diff=hex
|
||||
*.png binary diff=hex
|
||||
*.bin binary diff=hex
|
||||
*.elf binary diff=hex
|
||||
*.xz binary diff=hex
|
||||
*.zip binary diff=hex
|
||||
*.dtb binary diff=hex
|
||||
*.dtbo binary diff=hex
|
||||
*.dat binary diff=hex
|
||||
*.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
|
9
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,5 +1,6 @@
|
||||
- **Etcher version:**
|
||||
- **Operating system and architecture:**
|
||||
- **Do you see any meaningful error information on DevTools?**
|
||||
- **Etcher version:**
|
||||
- **Operating system and architecture:**
|
||||
- **Image flashed:**
|
||||
- **Do you see any meaningful error information in the DevTools?**
|
||||
|
||||
<!-- You can open DevTools by pressing `Ctrl+Alt+I`, or `Cmd+Alt+I` if you're running OS X. -->
|
||||
<!-- 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. -->
|
||||
|
24
.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
# Logs
|
||||
logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
@@ -9,10 +9,13 @@ pids
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
/lib-cov
|
||||
|
||||
# Image stream output directory
|
||||
/tests/image-stream/output
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
/coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
@@ -21,16 +24,17 @@ coverage
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
/generated
|
||||
|
||||
# Dependency directory
|
||||
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
# Compiled Electron releases
|
||||
release/
|
||||
etcher-release/
|
||||
# Compiled Etcher releases
|
||||
/dist
|
||||
|
||||
# Certificates
|
||||
*.spc
|
||||
@@ -39,3 +43,7 @@ etcher-release/
|
||||
*.cer
|
||||
*.crt
|
||||
*.pem
|
||||
|
||||
# OSX files
|
||||
|
||||
.DS_Store
|
||||
|
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
|
116
.resinci.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"electron": {
|
||||
"npm_version": "6.7.0",
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
"libusb-1.0-0-dev",
|
||||
"libyaml-dev",
|
||||
"libgtk-3-0",
|
||||
"libatk-bridge2.0-0",
|
||||
"libdbus-1-3",
|
||||
"libc6"
|
||||
]
|
||||
},
|
||||
"builder": {
|
||||
"appId": "io.balena.etcher",
|
||||
"copyright": "Copyright 2016-2019 Balena Ltd",
|
||||
"productName": "balenaEtcher",
|
||||
"nodeGypRebuild": true,
|
||||
"files": [
|
||||
"!node_modules/**/*.js.map",
|
||||
"!node_modules/**/*.h",
|
||||
"!node_modules/**/*.hpp",
|
||||
"!node_modules/**/*.cpp",
|
||||
"!node_modules/**/*.md",
|
||||
"!node_modules/**/*.ts",
|
||||
"!node_modules/**/*.coffee",
|
||||
"!node_modules/**/*.scss",
|
||||
"!node_modules/**/*.less",
|
||||
"!node_modules/**/*.hbs",
|
||||
"!node_modules/**/*.mkd",
|
||||
"!node_modules/**/LICENSE",
|
||||
"!node_modules/**/LICENCE",
|
||||
"!node_modules/**/license",
|
||||
"!node_modules/**/License",
|
||||
"!node_modules/**/LICENSE.txt",
|
||||
"!node_modules/**/Makefile",
|
||||
"!node_modules/**/.editorconfig",
|
||||
"!node_modules/**/.babelrc",
|
||||
"!node_modules/**/.prettierrc",
|
||||
"!node_modules/**/.prettierrc-*",
|
||||
"!node_modules/**/.eslintrc.yml",
|
||||
"!node_modules/**/.eslintignore",
|
||||
"!node_modules/**/.publishrc",
|
||||
"!lib/gui/app",
|
||||
"lib/gui/app/index.html",
|
||||
"generated",
|
||||
"!node_modules/chart.js/dist/docs",
|
||||
"!node_modules/ext2fs/config",
|
||||
"!node_modules/ext2fs/deps",
|
||||
"!node_modules/ext2fs/LICENSE",
|
||||
"!node_modules/ext2fs/src",
|
||||
"!node_modules/winusb-driver-generator/src",
|
||||
"!node_modules/winusb-driver-generator/deps",
|
||||
"!node_modules/winusb-driver-generator/ci",
|
||||
"!node_modules/rendition/__screenshots__",
|
||||
"!node_modules/polished/docs",
|
||||
"!node_modules/mermaid/src",
|
||||
"!node_modules/mermaid/dist",
|
||||
"node_modules/mermaid/dist/mermaid.core.js",
|
||||
"!node_modules/raven-js/src",
|
||||
"!node_modules/raven-js/dist",
|
||||
"node_modules/raven-js/dist/raven.js",
|
||||
"!node_modules/raven-js/plugins",
|
||||
"!node_modules/react-jsonschema-form/dist",
|
||||
"!node_modules/xxhash/deps",
|
||||
"!node_modules/xxhash/src",
|
||||
"!node_modules/unzip-stream/testData*",
|
||||
"!node_modules/usb",
|
||||
"node_modules/usb/usb.js",
|
||||
"node_modules/usb/package.json",
|
||||
"node_modules/usb/build",
|
||||
"node_modules/usb/src/binding",
|
||||
"!node_modules/roboto-fontface/fonts",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff"
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools"
|
||||
},
|
||||
"dmg": {
|
||||
"iconSize": 110,
|
||||
"contents": [
|
||||
{
|
||||
"x": 140,
|
||||
"y": 245
|
||||
},
|
||||
{
|
||||
"x": 415,
|
||||
"y": 245,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"width": 544,
|
||||
"height": 407
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
"packageCategory": "utils",
|
||||
"synopsis": "balenaEtcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more."
|
||||
},
|
||||
"deb": {
|
||||
"priority": "optional",
|
||||
"depends": [
|
||||
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
.sass-lint.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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
|
||||
|
@@ -1,16 +0,0 @@
|
||||
scss_files: 'lib/gui/scss/**/*.scss'
|
||||
|
||||
linters:
|
||||
Comment:
|
||||
enabled: false
|
||||
ImportantRule:
|
||||
enabled: false
|
||||
PlaceholderInExtend:
|
||||
enabled: false
|
||||
PropertySortOrder:
|
||||
enabled: false
|
||||
QualifyingElement:
|
||||
enabled: false
|
||||
StringQuotes:
|
||||
enabled: true
|
||||
style: double_quotes
|
70
.travis.yml
@@ -1,70 +0,0 @@
|
||||
language: cpp
|
||||
sudo: false
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
env:
|
||||
global:
|
||||
- NODE_VERSION="6.1.0"
|
||||
- RUBY_VERSION="2.3.1"
|
||||
matrix:
|
||||
- TARGET_ARCH=x64
|
||||
- TARGET_ARCH=x86
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
- os: osx
|
||||
env: TARGET_ARCH=x86
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
rm -rf ~/.nvm;
|
||||
git clone https://github.com/creationix/nvm.git ~/.nvm;
|
||||
source ~/.nvm/nvm.sh;
|
||||
nvm --version;
|
||||
nvm install $NODE_VERSION;
|
||||
node --version;
|
||||
npm --version;
|
||||
npm config set spin=false;
|
||||
rvm install $RUBY_VERSION;
|
||||
rvm use $RUBY_VERSION;
|
||||
fi
|
||||
|
||||
install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
pip install codespell;
|
||||
gem install scss_lint;
|
||||
npm install -g bower;
|
||||
brew install afsctool;
|
||||
brew install jq;
|
||||
fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
make info;
|
||||
make electron-develop;
|
||||
fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
|
||||
./scripts/build/docker/run-command.sh -r ${TARGET_ARCH} -s ${PWD} -c "make info && make electron-develop";
|
||||
fi
|
||||
|
||||
script:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
npm test;
|
||||
./scripts/ci/ensure-staged-sass.sh;
|
||||
fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
|
||||
./scripts/build/docker/run-command.sh -r ${TARGET_ARCH} -s ${PWD} -c "xvfb-run --server-args=$XVFB_ARGS npm test && ./scripts/ci/ensure-staged-sass.sh";
|
||||
fi
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/0a019c8b9828eb9f6a72
|
||||
on_success: change
|
||||
on_failure: always
|
||||
on_start: never
|
712
CHANGELOG.md
@@ -3,6 +3,714 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# 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
|
||||
|
||||
- Fix blob handling for usbboot
|
||||
|
||||
## v1.4.2 - 2018-04-18
|
||||
|
||||
### Features
|
||||
|
||||
- Make the progress button blue on verification
|
||||
- Display succeeded and failed devices on finish screen
|
||||
|
||||
### Fixes
|
||||
|
||||
- Exclude RAID devices from drive selection list
|
||||
- Display untitled device when device lacks description
|
||||
- Prefix multiple devices label with quantity
|
||||
- Fix handling of errors over IPC
|
||||
- Fix usbboot blob loading
|
||||
- Revert using native binding to clean disks on Windows
|
||||
|
||||
## v1.4.1 - 2018-04-10
|
||||
|
||||
### Fixes
|
||||
|
||||
- Exclude package.json from UI bundle
|
||||
|
||||
## v1.4.0 - 2018-04-05
|
||||
|
||||
### Features
|
||||
|
||||
- Move the drive selector warning dialog to the flash step
|
||||
- Display image size for comparison if drive is too small
|
||||
- Implement writing to multiple destinations simultaneously
|
||||
- Add colorised multi-writes progress status dots
|
||||
- Move CLI write preparation logic into SDK
|
||||
- Make the drive-selector button orange on warnings
|
||||
- Warn the user on selection of large drives
|
||||
- Consolidate low-level components into Etcher SDK
|
||||
- Use native code to clean drives on Windows
|
||||
- Increase UV_THREADPOOL_SIZE to allocate 4 threads per CPU
|
||||
- Add icon next to drive size when compatibility warnings exist
|
||||
- Display number of active devices while flashing in CLI
|
||||
- Replace CRC32 checksums with SHA512
|
||||
- Enable usbboot on Linux if run as root
|
||||
|
||||
### Fixes
|
||||
|
||||
- Improve spacing to the drive-selector warning/error labels
|
||||
- Line wrap selector size subtitles wholly
|
||||
- Hide the size label given multiple devices
|
||||
- Use correct usbboot blob path in AppImages
|
||||
- Fix EINVAL error on Linux
|
||||
- Fix enabling debug output
|
||||
- Fix DevTools opening in docked mode
|
||||
- Fix menu's application name
|
||||
- Fix "Array buffer allocation failed" when flashing some .dmg images
|
||||
- Log the banner load event to analytics
|
||||
- Warn on usbboot load error in the console on Linux
|
||||
- Ensure image/drive size is displayed on new line
|
||||
- Don't force-inherit process environment on Windows
|
||||
|
||||
### Misc
|
||||
|
||||
- Replace Helvetica as the main font with Roboto
|
||||
- Update Electron to v1.7.13
|
||||
- Add spacing to the drive warning icon
|
||||
- Use multi-drive methods with drive-list warning button
|
||||
- Remove unused & deprecated robot protocol
|
||||
- Update copyright years
|
||||
- Update instructions in ISSUE_TEMPLATE
|
||||
- Use Concourse CI for automated release builds
|
||||
- Only publish production packages to Bintray (remove devel)
|
||||
- Replace Gitter with Resin.io Forums for support
|
||||
- Add support for arm64 / armv8 / aarch64 in build scripts
|
||||
- Add descriptive name to modal popup windows
|
||||
|
||||
## v1.3.1 - 2018-01-23
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix "stdout maxBuffer" error on Linux
|
||||
- Fix Etcher not working / crashing on older Windows systems
|
||||
- Fix not all partitions being unmounted after flashing on Linux
|
||||
- Fix selection of images in folders with file extension on Mac OS
|
||||
|
||||
### Misc
|
||||
|
||||
- Update Electron to v1.7.11
|
||||
|
||||
## v1.3.0 - 2018-01-04
|
||||
|
||||
### Features
|
||||
|
||||
- Display connected Compute Modules even if Windows doesn't have the necessary drivers to act on them
|
||||
- Add read/write retry delays with backoff to ...
|
||||
- Add native application menu (which fixes OS native window management shortcuts not working)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix "Couldn't scan drives" error
|
||||
- Ensure the writer process dies when the GUI application is killed
|
||||
- Run elevated writing process asynchronously on Windows
|
||||
- Fix trailing space in environment variables during Windows elevation
|
||||
- Don't send analytics events when attempting to toggle a disabled drive
|
||||
- Fix handling of transient write errors on Linux (EBUSY)
|
||||
- Fix runaway perl process in drivelist on Mac OS
|
||||
|
||||
### Misc
|
||||
|
||||
- Update Electron from v1.7.9 to v1.7.10
|
||||
- Remove Angular dependency from image-writer
|
||||
|
||||
## v1.2.1 - 2017-12-06
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix handling of temporary read/write errors
|
||||
- Don't send initial Mixpanel events before "Anonymous Tracking" settings are loaded
|
||||
- Fix verification step reading from the cache
|
||||
|
||||
## v1.2.0 - 2017-11-22
|
||||
|
||||
### Features
|
||||
|
||||
- Display actual write speed
|
||||
- Add the progress and status to the window title.
|
||||
- Add a sudo-prompt upon launch on Linux-based systems.
|
||||
- Add optional progress bars to drive-selector drives.
|
||||
- Increase the flashing speed of usbboot discovered devices.
|
||||
- Add eye candy to usbboot initialized devices.
|
||||
- Integrate Raspberry Pi's usbboot technology.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix bzip2 streaming with the new pipelines
|
||||
- Remove Linux elevation meant for usbboot.
|
||||
- Fix `LIBUSB_ERROR_NO_DEVICE` error at the end of usbboot.
|
||||
- Gracefully handle scenarios where a USB drive is disconnected halfway through the usbboot procedure.
|
||||
- Make sure the progress button is always rounded.
|
||||
- Fix permission denied issues when XDG_RUNTIME_DIR is mounted with the `noexec` option.
|
||||
- Fix Etcher being unable to read certain zip files
|
||||
- Fix "Couldn't scan the drives: An unknown error occurred" error when there is a drive locked with BitLocker.
|
||||
- Fix "Missing state eta" error when speed is zero
|
||||
- Fix "Stuck on Starting..." error
|
||||
- Fix situations where the process would get stuck while flashing
|
||||
|
||||
### Misc
|
||||
|
||||
- Add the Python version (2.7) to the CONTRIBUTING doc.
|
||||
- Remove duplicate debug enabling in usbboot module.
|
||||
- Update Electron to v1.7.9
|
||||
- Retry ejection various times before giving up on Windows.
|
||||
- Try to use `$XDG_RUNTIME_DIR` to extract temporary scripts on GNU/Linux.
|
||||
|
||||
## v1.1.2 - 2017-08-07
|
||||
|
||||
### Features
|
||||
|
||||
- Add support for `.rpi-sdcard` images
|
||||
|
||||
### Fixes
|
||||
|
||||
- Avoid "broken" icon when selecting a zip image archive with invalid SVG
|
||||
- Fix `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` error at startup when behind certain proxies
|
||||
- Fix `EHOSTDOWN` error at startup
|
||||
- Display a user-friendly error message if the user is not in the sudoers file
|
||||
- Make archive-embedded SVG icons work again
|
||||
- Fix "imageBasename is not defined" error on the CLI
|
||||
- Fix various drive scanning Windows errors
|
||||
|
||||
### Misc
|
||||
|
||||
- Improve Windows drive detection error codes.
|
||||
|
||||
## v1.1.1 - 2017-07-25
|
||||
|
||||
### Fixes
|
||||
|
||||
- Prevent "percentage above 100%" errors on DMG images
|
||||
- Fix Etcher not starting flashes in AppImages
|
||||
- Fix most "Unmount failed" errors on macOS
|
||||
|
||||
## v1.1.0 - 2017-07-20
|
||||
|
||||
### Features
|
||||
|
||||
- Add image name, drive name, and icon to OS notifications
|
||||
- Add support for `.sdcard` images
|
||||
- Start publishing RPM packages
|
||||
- Generate single-binary portable installers on Windows
|
||||
- Show friendlier error dialogs when opening an image results in an error
|
||||
- Generate one-click Windows NSIS installers
|
||||
- Show the application version in the WebView banners
|
||||
- Show a warning message if the selected image has no partition table
|
||||
- Make use of `pkg` to package the Etcher CLI
|
||||
- Send anonymous analytics about package types
|
||||
- Minor style improvements to the fallback success page banner
|
||||
- Turn the update notifier modal into a native dialog
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix "You don't have access to this resource" error at startup when behind a firewall
|
||||
- Fix `UNABLE_TO_VERIFY_LEAF_SIGNATURE` error at startup when behind a proxy
|
||||
- Reset webview after navigating away from the success screen
|
||||
- Fix occasional increased CPU usage because of perl regular expression in macOS
|
||||
- Don't install to `C:\Program Files (x86)` on 64-bit Windows systems
|
||||
- Fix "file is not accessible" error when flashing an image that lives inside a directory whose name is UTF-16 encoded on Windows.
|
||||
- Fix various interrelated Windows `.bat` spawning issues
|
||||
- Fix 0.0 GB Windows drive detection issues
|
||||
- Cleanup drive detection temporary scripts in GNU/Linux and macOS
|
||||
- Ensure no analytics events are sent if error reporting is disabled
|
||||
- Retry various times on `EAGAIN` when spawning drive scanning scripts
|
||||
- Don't break up size numbers in the drive selector
|
||||
|
||||
### Misc
|
||||
|
||||
- Remove "Advanced" settings subtitle
|
||||
- Remove support for the `ETCHER_DISABLE_UPDATES` environment variable
|
||||
- Swap speed and time below the flashing progress bar
|
||||
|
||||
## v1.0.0 - 2017-05-12
|
||||
|
||||
### Features
|
||||
|
||||
- Implement a dynamic finish page.
|
||||
- Display nicer error dialog when reading an invalid image.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Prevent drive from getting re-mounted in macOS even when the unmount on success setting is enabled.
|
||||
- Fix `ECONNRESET` and `ECONNREFUSED` errors when checking for updates on unstable connections.
|
||||
- Fix application stuck at "Starting..." on Windows.
|
||||
- Fix error on startup when Windows username contained an ampersand.
|
||||
|
||||
## v1.0.0-rc.5 - 2017-05-02
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix various elevation issues on Windows
|
||||
- Treat unknown images as octet stream
|
||||
- Fix uncaught errors when cancelling elevation requests on Windows when the system's language is not English.
|
||||
|
||||
## v1.0.0-rc.4 - 2017-04-22
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix "Unmount failed" on Windows where the PC is connected to network drives.
|
||||
- Various fixes for when drive descriptions contain special characters.
|
||||
|
||||
### Misc
|
||||
|
||||
- Show a friendly user message on EIO after many retries.
|
||||
- Show user friendly messages for `EBUSY, read` and `EBUSY, write` errors on macOS.
|
||||
|
||||
## v1.0.0-rc.3 - 2017-04-14
|
||||
|
||||
### Fixes
|
||||
|
||||
- Show a user friendly message when the drive is unplugged half-way through.
|
||||
- Fix "UNKNOWN: unknown error" error when unplugging an SD Card from an internal reader on Windows.
|
||||
- Fix "function createError(opts) {}" error on validation failure.
|
||||
- Fix "Unmount failed, invalid drive" error on Windows.
|
||||
- Fix Apple disk image detection & streaming.
|
||||
|
||||
### Misc
|
||||
|
||||
- Improve error reporting accuracy.
|
||||
|
||||
## v1.0.0-rc.2 - 2017-04-11
|
||||
|
||||
### Fixes
|
||||
|
||||
- Display a user error if the image is no longer accessible when the writer starts.
|
||||
- Prevent uncaught `EISDIR` when dropping a directory to the application.
|
||||
- Fix "Path must be a string. Received undefined" when selecting Apple images.
|
||||
- Don't interpret certain ISO images as unsupported.
|
||||
|
||||
## v1.0.0-rc.1 - 2017-04-10
|
||||
|
||||
### Features
|
||||
|
||||
- Add support for Apple Disk images.
|
||||
- Add the un-truncated drive description to the selected drive step tooltip.
|
||||
- Prevent flashing an image that is larger than the drive with the CLI.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Prevent progress button percentage to exceed 100%.
|
||||
- Don't print stack traces by default in the CLI.
|
||||
- Prevent blank application when sending SIGINT on GNU/Linux and macOS.
|
||||
- Fix unmounting freezing in macOS.
|
||||
- Fix GNU/Linux udev error when `net.ifnames` is set.
|
||||
- Fix `ENOSPC` image alignment errors.
|
||||
- Fix errors when unplugging drives exactly when the drive scanning scripts are running.
|
||||
- Fix several unmount related issues in all platforms.
|
||||
- Fix "rawr i'm a dinosaur" bzip2 error.
|
||||
|
||||
### Misc
|
||||
|
||||
- Make errors more user friendly throughout the application.
|
||||
- Don't report "invalid archive" errors to TrackJS.
|
||||
- Stop drive scanning loop if an error occurs.
|
||||
- Don't include user paths in Mixpanel analytics events.
|
||||
- Provide a user friendly error message when no polkit authentication agent is available on the system.
|
||||
- Show friendly drive name instead of device name in the main screen.
|
||||
- Start reporting errors to Sentry instead of to TrackJS.
|
||||
|
||||
## v1.0.0-beta.19 - 2017-02-24
|
||||
|
||||
### Features
|
||||
@@ -258,8 +966,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
### Fixes
|
||||
|
||||
- Fix broken image drag and drop functionality.
|
||||
- Prevent global shortcuts from interferring with another applications.
|
||||
- Prevent re-activating the "Flash" button with the keybaord shortcuts when a flash is already in process.
|
||||
- Prevent global shortcuts from interfering with another applications.
|
||||
- Prevent re-activating the "Flash" button with the keyboard shortcuts when a flash is already in process.
|
||||
- Fix certain non-removable Windows devices not being filtered out.
|
||||
- Display non-mountable Windows drives in the drive selector.
|
||||
|
||||
|
2
CODEOWNERS
Normal file
@@ -0,0 +1,2 @@
|
||||
* @thundron @zvin @jviotti
|
||||
/scripts @nazrhom
|
462
Makefile
@@ -2,8 +2,12 @@
|
||||
# Build configuration
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
RESIN_SCRIPTS ?= ./scripts/resin
|
||||
export NPM_VERSION ?= 6.7.0
|
||||
S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||
|
||||
# This directory will be completely deleted by the `clean` rule
|
||||
BUILD_DIRECTORY ?= release
|
||||
BUILD_DIRECTORY ?= dist
|
||||
|
||||
# See http://stackoverflow.com/a/20763842/1641422
|
||||
BUILD_DIRECTORY_PARENT = $(dir $(BUILD_DIRECTORY))
|
||||
@@ -12,34 +16,16 @@ $(error $(BUILD_DIRECTORY_PARENT) does not exist)
|
||||
endif
|
||||
|
||||
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
|
||||
BUILD_OUTPUT_DIRECTORY = $(BUILD_DIRECTORY)/out
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Application configuration
|
||||
# ---------------------------------------------------------------------
|
||||
$(BUILD_DIRECTORY):
|
||||
mkdir $@
|
||||
|
||||
ELECTRON_VERSION = $(shell jq -r '.devDependencies["electron-prebuilt"]' package.json)
|
||||
COMPANY_NAME = $(shell jq -r '.companyName' package.json)
|
||||
APPLICATION_NAME = $(shell jq -r '.displayName' package.json)
|
||||
APPLICATION_DESCRIPTION = $(shell jq -r '.description' package.json)
|
||||
APPLICATION_COPYRIGHT = $(shell jq -r '.copyright' package.json)
|
||||
APPLICATION_CATEGORY = public.app-category.developer-tools
|
||||
APPLICATION_BUNDLE_ID = io.resin.etcher
|
||||
APPLICATION_FILES = lib,assets
|
||||
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
|
||||
# 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)
|
||||
endif
|
||||
ifeq ($(RELEASE_TYPE),snapshot)
|
||||
CURRENT_COMMIT_HASH = $(shell git log -1 --format="%h")
|
||||
APPLICATION_VERSION = $(PACKAGE_JSON_VERSION)+$(CURRENT_COMMIT_HASH)
|
||||
endif
|
||||
ifndef APPLICATION_VERSION
|
||||
$(error Invalid release type: $(RELEASE_TYPE))
|
||||
endif
|
||||
# See https://stackoverflow.com/a/13468229/1641422
|
||||
SHELL := /bin/bash
|
||||
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Operating system and architecture detection
|
||||
@@ -47,7 +33,7 @@ endif
|
||||
|
||||
# http://stackoverflow.com/a/12099167
|
||||
ifeq ($(OS),Windows_NT)
|
||||
HOST_PLATFORM = win32
|
||||
PLATFORM = win32
|
||||
|
||||
ifeq ($(PROCESSOR_ARCHITEW6432),AMD64)
|
||||
HOST_ARCH = x64
|
||||
@@ -61,17 +47,29 @@ ifeq ($(OS),Windows_NT)
|
||||
endif
|
||||
else
|
||||
ifeq ($(shell uname -s),Linux)
|
||||
HOST_PLATFORM = linux
|
||||
PLATFORM = linux
|
||||
|
||||
ifeq ($(shell uname -p),x86_64)
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
HOST_ARCH = x64
|
||||
endif
|
||||
ifneq ($(filter %86,$(shell uname -p)),)
|
||||
ifneq ($(filter %86,$(shell uname -m)),)
|
||||
HOST_ARCH = x86
|
||||
endif
|
||||
ifeq ($(shell uname -m),armv7l)
|
||||
HOST_ARCH = armv7hf
|
||||
endif
|
||||
ifeq ($(shell uname -m),aarch64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
ifeq ($(shell uname -m),armv8)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
ifeq ($(shell uname -m),arm64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
endif
|
||||
ifeq ($(shell uname -s),Darwin)
|
||||
HOST_PLATFORM = darwin
|
||||
PLATFORM = darwin
|
||||
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
HOST_ARCH = x64
|
||||
@@ -79,17 +77,11 @@ else
|
||||
endif
|
||||
endif
|
||||
|
||||
ifndef HOST_PLATFORM
|
||||
$(error We couldn't detect your host platform)
|
||||
ifndef PLATFORM
|
||||
$(error We could not detect your host platform)
|
||||
endif
|
||||
ifndef HOST_ARCH
|
||||
$(error We couldn't detect your host architecture)
|
||||
endif
|
||||
|
||||
TARGET_PLATFORM = $(HOST_PLATFORM)
|
||||
|
||||
ifneq ($(TARGET_PLATFORM),$(HOST_PLATFORM))
|
||||
$(error We don't support cross-platform builds yet)
|
||||
$(error We could not detect your host architecture)
|
||||
endif
|
||||
|
||||
# Default to host architecture. You can override by doing:
|
||||
@@ -98,230 +90,32 @@ endif
|
||||
#
|
||||
TARGET_ARCH ?= $(HOST_ARCH)
|
||||
|
||||
# Support x86 builds from x64 in GNU/Linux
|
||||
# See https://github.com/addaleax/lzma-native/issues/27
|
||||
ifeq ($(TARGET_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
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Code signing
|
||||
# Electron
|
||||
# ---------------------------------------------------------------------
|
||||
electron-develop: | $(BUILD_TEMPORARY_DIRECTORY)
|
||||
$(RESIN_SCRIPTS)/electron/install.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-s $(PLATFORM) \
|
||||
-m $(NPM_VERSION)
|
||||
|
||||
ifeq ($(TARGET_PLATFORM),darwin)
|
||||
ifndef CODE_SIGN_IDENTITY
|
||||
$(warning No code-sign identity found (CODE_SIGN_IDENTITY is not set))
|
||||
endif
|
||||
endif
|
||||
electron-test:
|
||||
$(RESIN_SCRIPTS)/electron/test.sh \
|
||||
-b $(shell pwd) \
|
||||
-s $(PLATFORM)
|
||||
|
||||
ifeq ($(TARGET_PLATFORM),win32)
|
||||
ifndef CODE_SIGN_CERTIFICATE
|
||||
$(warning No code-sign certificate found (CODE_SIGN_CERTIFICATE is not set))
|
||||
ifndef CODE_SIGN_CERTIFICATE_PASSWORD
|
||||
$(warning No code-sign certificate password found (CODE_SIGN_CERTIFICATE_PASSWORD is not set))
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
|
||||
tiffutil -cathidpicheck $^ -out $@
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Extra variables
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
ifeq ($(TARGET_ARCH),x86)
|
||||
TARGET_ARCH_DEBIAN = i386
|
||||
endif
|
||||
ifeq ($(TARGET_ARCH),x64)
|
||||
TARGET_ARCH_DEBIAN = amd64
|
||||
endif
|
||||
|
||||
ifeq ($(RELEASE_TYPE),production)
|
||||
PRODUCT_NAME = etcher
|
||||
endif
|
||||
ifeq ($(RELEASE_TYPE),snapshot)
|
||||
PRODUCT_NAME = etcher-snapshots
|
||||
endif
|
||||
|
||||
APPLICATION_NAME_LOWERCASE = $(shell echo $(APPLICATION_NAME) | tr A-Z a-z)
|
||||
APPLICATION_VERSION_DEBIAN = $(shell echo $(APPLICATION_VERSION) | tr "-" "~")
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 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 $@
|
||||
|
||||
$(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(TARGET_ARCH)-dependencies: | $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
|
||||
$(BUILD_OUTPUT_DIRECTORY): | $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
|
||||
$(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(TARGET_ARCH)-dependencies/node_modules: package.json npm-shrinkwrap.json \
|
||||
| $(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(TARGET_ARCH)-dependencies
|
||||
./scripts/build/dependencies-npm.sh -p \
|
||||
-r "$(TARGET_ARCH)" \
|
||||
-v "$(ELECTRON_VERSION)" \
|
||||
-x $| \
|
||||
-t electron \
|
||||
-s "$(TARGET_PLATFORM)"
|
||||
|
||||
$(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(TARGET_ARCH)-dependencies/bower_components: bower.json \
|
||||
| $(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(TARGET_ARCH)-dependencies
|
||||
./scripts/build/dependencies-bower.sh -p -x $|
|
||||
|
||||
$(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(APPLICATION_VERSION)-$(TARGET_ARCH)-app: \
|
||||
$(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(TARGET_ARCH)-dependencies/node_modules \
|
||||
$(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(TARGET_ARCH)-dependencies/bower_components \
|
||||
| $(BUILD_DIRECTORY)
|
||||
./scripts/build/electron-create-resources-app.sh -s . -o $@ \
|
||||
-v $(APPLICATION_VERSION) \
|
||||
-f "$(APPLICATION_FILES)"
|
||||
$(foreach prerequisite,$^,$(call execute-command,cp -rf $(prerequisite) $@))
|
||||
|
||||
$(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(APPLICATION_VERSION)-$(TARGET_ARCH)-app.asar: \
|
||||
$(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(APPLICATION_VERSION)-$(TARGET_ARCH)-app \
|
||||
| $(BUILD_DIRECTORY)
|
||||
./scripts/build/electron-create-asar.sh -d $< -o $@
|
||||
|
||||
$(BUILD_DIRECTORY)/electron-$(ELECTRON_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).zip: \
|
||||
| $(BUILD_DIRECTORY)
|
||||
./scripts/build/electron-download-package.sh \
|
||||
-r "$(TARGET_ARCH)" \
|
||||
-v "$(ELECTRON_VERSION)" \
|
||||
-s "$(TARGET_PLATFORM)" \
|
||||
-o $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH): \
|
||||
$(BUILD_DIRECTORY)/electron-$(TARGET_PLATFORM)-$(APPLICATION_VERSION)-$(TARGET_ARCH)-app.asar \
|
||||
$(BUILD_DIRECTORY)/electron-$(ELECTRON_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).zip \
|
||||
| $(BUILD_DIRECTORY) $(BUILD_TEMPORARY_DIRECTORY)
|
||||
ifeq ($(TARGET_PLATFORM),darwin)
|
||||
./scripts/build/electron-configure-package-darwin.sh -p $(word 2,$^) -a $< \
|
||||
-n "$(APPLICATION_NAME)" \
|
||||
-v "$(APPLICATION_VERSION)" \
|
||||
-b "$(APPLICATION_BUNDLE_ID)" \
|
||||
-c "$(APPLICATION_COPYRIGHT)" \
|
||||
-t "$(APPLICATION_CATEGORY)" \
|
||||
-i assets/icon.icns \
|
||||
-o $@
|
||||
endif
|
||||
ifeq ($(TARGET_PLATFORM),linux)
|
||||
./scripts/build/electron-configure-package-linux.sh -p $(word 2,$^) -a $< \
|
||||
-n "$(APPLICATION_NAME)" \
|
||||
-v "$(APPLICATION_VERSION)" \
|
||||
-l LICENSE \
|
||||
-o $@
|
||||
endif
|
||||
ifeq ($(TARGET_PLATFORM),win32)
|
||||
./scripts/build/electron-configure-package-win32.sh -p $(word 2,$^) -a $< \
|
||||
-n "$(APPLICATION_NAME)" \
|
||||
-d "$(APPLICATION_DESCRIPTION)" \
|
||||
-v "$(APPLICATION_VERSION)" \
|
||||
-l LICENSE \
|
||||
-c "$(APPLICATION_COPYRIGHT)" \
|
||||
-m "$(COMPANY_NAME)" \
|
||||
-i assets/icon.ico \
|
||||
-w $(BUILD_TEMPORARY_DIRECTORY) \
|
||||
-o $@
|
||||
ifdef CODE_SIGN_CERTIFICATE
|
||||
ifdef CODE_SIGN_CERTIFICATE_PASSWORD
|
||||
./scripts/build/electron-sign-exe.sh -f $@/$(APPLICATION_NAME).exe \
|
||||
-d "$(APPLICATION_NAME) - $(APPLICATION_VERSION)"
|
||||
-c $(CODE_SIGN_CERTIFICATE) \
|
||||
-p $(CODE_SIGN_CERTIFICATE_PASSWORD)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH)-rw.dmg: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-darwin-$(TARGET_ARCH) \
|
||||
| $(BUILD_DIRECTORY)
|
||||
./scripts/build/electron-create-readwrite-dmg-darwin.sh -p $< -o $@ \
|
||||
-n "$(APPLICATION_NAME)" \
|
||||
-i assets/icon.icns \
|
||||
-b assets/osx/installer.png
|
||||
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-darwin-$(TARGET_ARCH).zip: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-darwin-$(TARGET_ARCH) \
|
||||
| $(BUILD_OUTPUT_DIRECTORY)
|
||||
ifdef CODE_SIGN_IDENTITY
|
||||
./scripts/build/electron-sign-app-darwin.sh -a $</$(APPLICATION_NAME).app -i "$(CODE_SIGN_IDENTITY)"
|
||||
endif
|
||||
./scripts/build/electron-installer-app-zip-darwin.sh -a $</$(APPLICATION_NAME).app -o $@
|
||||
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-darwin-$(TARGET_ARCH).dmg: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH)-rw.dmg \
|
||||
| $(BUILD_OUTPUT_DIRECTORY)
|
||||
ifdef CODE_SIGN_IDENTITY
|
||||
./scripts/build/electron-sign-dmg-darwin.sh \
|
||||
-n "$(APPLICATION_NAME)" \
|
||||
-d $< \
|
||||
-i "$(CODE_SIGN_IDENTITY)"
|
||||
endif
|
||||
./scripts/build/electron-create-readonly-dmg-darwin.sh -d $< -o $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-linux-$(TARGET_ARCH).AppDir: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-linux-$(TARGET_ARCH) \
|
||||
| $(BUILD_DIRECTORY)
|
||||
./scripts/build/electron-create-appdir.sh -p $< -o $@ \
|
||||
-n "$(APPLICATION_NAME)" \
|
||||
-d "$(APPLICATION_DESCRIPTION)" \
|
||||
-r "$(TARGET_ARCH)" \
|
||||
-b "$(APPLICATION_NAME_LOWERCASE)" \
|
||||
-i assets/icon.png
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-linux-$(TARGET_ARCH).AppImage: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-linux-$(TARGET_ARCH).AppDir \
|
||||
| $(BUILD_DIRECTORY) $(BUILD_TEMPORARY_DIRECTORY)
|
||||
./scripts/build/electron-create-appimage-linux.sh -d $< -o $@ \
|
||||
-r "$(TARGET_ARCH)" \
|
||||
-w "$(BUILD_TEMPORARY_DIRECTORY)"
|
||||
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-linux-$(TARGET_ARCH).zip: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-linux-$(TARGET_ARCH).AppImage \
|
||||
| $(BUILD_OUTPUT_DIRECTORY)
|
||||
./scripts/build/electron-installer-appimage-zip.sh -i $< -o $@
|
||||
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-electron_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-linux-$(TARGET_ARCH) \
|
||||
| $(BUILD_OUTPUT_DIRECTORY)
|
||||
./scripts/build/electron-installer-debian-linux.sh -p $< -r "$(TARGET_ARCH)" -o $| \
|
||||
-c scripts/build/debian/config.json
|
||||
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-win32-$(TARGET_ARCH).zip: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-win32-$(TARGET_ARCH) \
|
||||
| $(BUILD_OUTPUT_DIRECTORY)
|
||||
./scripts/build/electron-installer-zip-win32.sh -a $< -o $@
|
||||
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-win32-$(TARGET_ARCH).exe: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-win32-$(TARGET_ARCH) \
|
||||
| $(BUILD_OUTPUT_DIRECTORY) $(BUILD_TEMPORARY_DIRECTORY)
|
||||
./scripts/build/electron-installer-nsis-win32.sh -n $(APPLICATION_NAME) -a $< -t $(BUILD_TEMPORARY_DIRECTORY) -o $@
|
||||
ifdef CODE_SIGN_CERTIFICATE
|
||||
ifdef CODE_SIGN_CERTIFICATE_PASSWORD
|
||||
./scripts/build/electron-sign-exe.sh -f $@ \
|
||||
-d "$(APPLICATION_NAME) - $(APPLICATION_VERSION)"
|
||||
-c $(CODE_SIGN_CERTIFICATE) \
|
||||
-p $(CODE_SIGN_CERTIFICATE_PASSWORD)
|
||||
endif
|
||||
endif
|
||||
electron-build: assets/dmg/background.tiff | $(BUILD_TEMPORARY_DIRECTORY)
|
||||
$(RESIN_SCRIPTS)/electron/build.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-s $(PLATFORM) \
|
||||
-v production \
|
||||
-n $(BUILD_TEMPORARY_DIRECTORY)/npm \
|
||||
-w $(BUILD_TEMPORARY_DIRECTORY)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Phony targets
|
||||
@@ -330,105 +124,93 @@ endif
|
||||
TARGETS = \
|
||||
help \
|
||||
info \
|
||||
lint \
|
||||
lint-js \
|
||||
lint-sass \
|
||||
lint-cpp \
|
||||
lint-html \
|
||||
lint-spell \
|
||||
test-spectron \
|
||||
test-gui \
|
||||
test \
|
||||
sanity-checks \
|
||||
clean \
|
||||
distclean \
|
||||
package \
|
||||
electron-develop
|
||||
webpack \
|
||||
electron-develop \
|
||||
electron-test \
|
||||
electron-build
|
||||
|
||||
package: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH)
|
||||
|
||||
ifeq ($(TARGET_PLATFORM),darwin)
|
||||
electron-installer-app-zip: $(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).zip
|
||||
electron-installer-dmg: $(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).dmg
|
||||
TARGETS += \
|
||||
electron-installer-dmg \
|
||||
electron-installer-app-zip
|
||||
PUBLISH_AWS_S3 += \
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).zip \
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).dmg
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_PLATFORM),linux)
|
||||
electron-installer-appimage: $(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).zip
|
||||
electron-installer-debian: $(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-electron_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
|
||||
TARGETS += \
|
||||
electron-installer-appimage \
|
||||
electron-installer-debian
|
||||
PUBLISH_AWS_S3 += \
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).zip
|
||||
PUBLISH_BINTRAY_DEBIAN += \
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-electron_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_PLATFORM),win32)
|
||||
electron-installer-zip: $(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).zip
|
||||
electron-installer-nsis: $(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-win32-$(TARGET_ARCH).exe
|
||||
TARGETS += \
|
||||
electron-installer-zip \
|
||||
electron-installer-nsis
|
||||
PUBLISH_AWS_S3 += \
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-$(TARGET_PLATFORM)-$(TARGET_ARCH).zip \
|
||||
$(BUILD_OUTPUT_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-win32-$(TARGET_ARCH).exe
|
||||
endif
|
||||
|
||||
ifdef PUBLISH_AWS_S3
|
||||
publish-aws-s3: $(PUBLISH_AWS_S3)
|
||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/aws-s3.sh \
|
||||
-f $(publishable) \
|
||||
-b $(S3_BUCKET) \
|
||||
-v $(APPLICATION_VERSION) \
|
||||
-p $(PRODUCT_NAME)))
|
||||
|
||||
TARGETS += publish-aws-s3
|
||||
endif
|
||||
|
||||
ifdef PUBLISH_BINTRAY_DEBIAN
|
||||
publish-bintray-debian: $(PUBLISH_BINTRAY_DEBIAN)
|
||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/bintray-debian.sh \
|
||||
-f $(publishable) \
|
||||
-v $(APPLICATION_VERSION_DEBIAN) \
|
||||
-r $(TARGET_ARCH_DEBIAN) \
|
||||
-c $(APPLICATION_NAME_LOWERCASE) \
|
||||
-t $(RELEASE_TYPE)))
|
||||
|
||||
TARGETS += publish-bintray-debian
|
||||
endif
|
||||
webpack:
|
||||
./node_modules/.bin/webpack
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
|
||||
electron-develop:
|
||||
# Since we use an `npm-shrinkwrap.json` file, if you pull changes
|
||||
# that update a dependency and try to `npm install` directly, npm
|
||||
# will complain that your `node_modules` tree is not equal to what
|
||||
# is defined by the `npm-shrinkwrap.json` file, and will thus
|
||||
# refuse to do anything but install from scratch.
|
||||
# The `node_modules` directory also needs to be wiped out if you're
|
||||
# changing between target architectures, since compiled add-ons
|
||||
# will not work otherwise.
|
||||
rm -rf node_modules
|
||||
./scripts/build/dependencies-npm.sh \
|
||||
-r "$(TARGET_ARCH)" \
|
||||
-v "$(ELECTRON_VERSION)" \
|
||||
-t electron \
|
||||
-s "$(TARGET_PLATFORM)"
|
||||
./scripts/build/dependencies-bower.sh
|
||||
sass:
|
||||
npm rebuild node-sass
|
||||
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
|
||||
|
||||
lint-ts:
|
||||
resin-lint --typescript lib
|
||||
|
||||
lint-js:
|
||||
eslint --ignore-pattern scripts/resin/**/*.js lib tests scripts bin 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 *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
|
||||
lib tests docs scripts Makefile *.md LICENSE
|
||||
|
||||
lint: lint-ts lint-js lint-sass lint-cpp lint-html lint-spell
|
||||
|
||||
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register
|
||||
|
||||
# See https://github.com/electron/spectron/issues/127
|
||||
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
|
||||
test-spectron:
|
||||
ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron
|
||||
|
||||
test-gui:
|
||||
electron-mocha $(MOCHA_OPTIONS) --renderer tests/gui
|
||||
|
||||
test-sdk:
|
||||
electron-mocha $(MOCHA_OPTIONS) \
|
||||
tests/shared
|
||||
|
||||
test: test-gui test-sdk test-spectron
|
||||
|
||||
help:
|
||||
@echo "Available targets: $(TARGETS)"
|
||||
|
||||
info:
|
||||
@echo "Application version : $(APPLICATION_VERSION)"
|
||||
@echo "Release type : $(RELEASE_TYPE)"
|
||||
@echo "Host platform : $(HOST_PLATFORM)"
|
||||
@echo "Platform : $(PLATFORM)"
|
||||
@echo "Host arch : $(HOST_ARCH)"
|
||||
@echo "Target platform : $(TARGET_PLATFORM)"
|
||||
@echo "Target arch : $(TARGET_ARCH)"
|
||||
|
||||
sanity-checks:
|
||||
./scripts/ci/ensure-staged-sass.sh
|
||||
./scripts/ci/ensure-npm-dependencies-compatibility.sh
|
||||
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIRECTORY)
|
||||
|
||||
distclean: clean
|
||||
rm -rf node_modules
|
||||
rm -rf bower_components
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
rm -rf generated
|
||||
rm -rf $(BUILD_TEMPORARY_DIRECTORY)
|
||||
|
||||
.DEFAULT_GOAL = help
|
||||
|
139
README.md
@@ -1,63 +1,110 @@
|
||||
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://david-dm.org/resin-io/etcher.svg)
|
||||
[](https://travis-ci.org/resin-io/etcher)
|
||||
[](https://ci.appveyor.com/project/resin-io/etcher/branch/master)
|
||||
[](https://gitter.im/resin-io/etcher?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](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.
|
||||
|
||||
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Save the following as `/etc/apt/sources.list.d/etcher.list`:
|
||||
1. Add Etcher debian repository:
|
||||
|
||||
```
|
||||
deb https://dl.bintray.com/resin-io/debian stable etcher
|
||||
```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 keyserver.ubuntu.com --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 balena-etcher-electron
|
||||
sudo rm /etc/apt/sources.list.d/balena-etcher.list
|
||||
sudo apt-get update
|
||||
```
|
||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo yum clean all
|
||||
sudo yum makecache fast
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
```
|
||||
|
||||
#### Solus (GNU/Linux x64)
|
||||
|
||||
```sh
|
||||
sudo eopkg it etcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo eopkg rm etcher
|
||||
```
|
||||
|
||||
#### Brew Cask (macOS)
|
||||
|
||||
Note that the Etcher Cask has to be updated manually to point to new versions,
|
||||
@@ -65,28 +112,46 @@ so it might not refer to the latest version immediately after an Etcher
|
||||
release.
|
||||
|
||||
```sh
|
||||
brew cask install etcher
|
||||
brew cask install balenaetcher
|
||||
```
|
||||
|
||||
Support
|
||||
-------
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
brew cask uninstall balenaetcher
|
||||
```
|
||||
|
||||
#### Chocolatey (Windows)
|
||||
|
||||
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
|
||||
is kept up to date automatically.
|
||||
|
||||
```sh
|
||||
choco install etcher
|
||||
```
|
||||
|
||||
##### 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
|
||||
|
17
SUPPORT.md
@@ -4,12 +4,13 @@ Getting help with Etcher
|
||||
There are various ways to get support for Etcher if you experience an issue or
|
||||
have an idea you'd like to share with us.
|
||||
|
||||
Gitter
|
||||
Forums
|
||||
------
|
||||
|
||||
We have a [Gitter chat room][gitter] for Etcher which is open to everyone,
|
||||
please come join us :). Drop us a line there and the resin.io staff and
|
||||
community users will be happy to assist.
|
||||
We have a [Discourse forum][discourse] which is open to everyone, so please
|
||||
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!
|
||||
|
||||
Make sure to mention the following information to help us provide better
|
||||
support:
|
||||
@@ -19,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
|
||||
------
|
||||
@@ -28,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].
|
||||
|
||||
[gitter]: https://gitter.im/resin-io/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
|
||||
|
55
appveyor.yml
@@ -1,55 +0,0 @@
|
||||
# appveyor file
|
||||
# http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
init:
|
||||
- git config --global core.autocrlf input
|
||||
|
||||
image: Visual Studio 2015
|
||||
|
||||
cache:
|
||||
- C:\Users\appveyor\.node-gyp
|
||||
- '%AppData%\npm-cache'
|
||||
- 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.1.0
|
||||
matrix:
|
||||
- TARGET_ARCH: x64
|
||||
- TARGET_ARCH: x86
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- npm install -g bower rimraf asar
|
||||
- choco install nsis -version 2.51
|
||||
- choco install upx
|
||||
- choco install jq
|
||||
- set PATH=C:\Program Files (x86)\Windows Kits\8.1\bin\x86;%PATH%
|
||||
- set PATH=C:\Program Files (x86)\NSIS;%PATH%
|
||||
- set PATH=C:\Ruby22\bin;%PATH%
|
||||
- set PATH=C:\MinGW\bin;%PATH%
|
||||
- set PATH=C:\MinGW\msys\1.0\bin;%PATH%
|
||||
- gem install scss_lint
|
||||
- pip install codespell
|
||||
- make info
|
||||
- make electron-develop
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- bash .\scripts\ci\ensure-staged-sass.sh
|
||||
- cmd: npm test
|
||||
|
||||
notifications:
|
||||
|
||||
- provider: Webhook
|
||||
url: https://webhooks.gitter.im/e/0becb34b32e20d389bb8
|
||||
on_build_success: false
|
||||
on_build_failure: true
|
||||
on_build_status_changed: true
|
BIN
assets/dmg/background.png
Executable file
After Width: | Height: | Size: 38 KiB |
BIN
assets/dmg/background.tiff
Executable file
BIN
assets/dmg/background@2x.png
Executable file
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 |
BIN
assets/iconset/128x128.png
Executable file
After Width: | Height: | Size: 3.0 KiB |
BIN
assets/iconset/16x16.png
Executable file
After Width: | Height: | Size: 479 B |
BIN
assets/iconset/256x256.png
Executable file
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/iconset/32x32.png
Executable file
After Width: | Height: | Size: 802 B |
BIN
assets/iconset/48x48.png
Executable file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/iconset/512x512.png
Executable file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 32 KiB |
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
require('../lib/cli/etcher');
|
35
binding.gyp
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "elevator",
|
||||
"include_dirs" : [
|
||||
"src",
|
||||
"<!(node -e \"require('nan')\")"
|
||||
],
|
||||
'conditions': [
|
||||
|
||||
[ 'OS=="win"', {
|
||||
"sources": [
|
||||
"src/utils/v8utils.cpp",
|
||||
"src/os/win32/elevate.cpp",
|
||||
"src/elevator_init.cpp",
|
||||
],
|
||||
"libraries": [
|
||||
"-lShell32.lib",
|
||||
],
|
||||
} ],
|
||||
|
||||
[ 'OS=="mac"', {
|
||||
"xcode_settings": {
|
||||
"OTHER_CPLUSPLUSFLAGS": [
|
||||
"-stdlib=libc++"
|
||||
],
|
||||
"OTHER_LDFLAGS": [
|
||||
"-stdlib=libc++"
|
||||
]
|
||||
}
|
||||
} ]
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
13
bower.json
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "etcher",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"angular-mixpanel": "~1.1.2"
|
||||
}
|
||||
}
|
4
dev-app-update.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
owner: balena-io
|
||||
repo: etcher
|
||||
provider: github
|
||||
updaterCacheDirName: balena-etcher-updater
|
@@ -1 +0,0 @@
|
||||
boolen->boolean
|
9
dictionary.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
boolen->boolean
|
||||
aknowledge->acknowledge
|
||||
seleted->selected
|
||||
reming->remind
|
||||
locl->local
|
||||
subsribe->subscribe
|
||||
unsubsribe->unsubscribe
|
||||
calcluate->calculate
|
||||
dictionaty->dictionary
|
@@ -40,64 +40,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](https://github.com/resin-io-modules/etcher-image-stream)
|
||||
|
||||
The goal of this project is to convert 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]
|
||||
|
||||
- An environment variable called `ETCHER_CLI_ROBOT` option, which when set
|
||||
causes the Etcher CLI to output state in a way that can be easily
|
||||
parsed by a machine.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
@@ -108,10 +62,8 @@ 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/gui/app/modules/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
|
||||
|
45
docs/CLI.md
@@ -1,45 +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.
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
We are not oficially releasing the Etcher CLI as a separate package yet, but
|
||||
you can run it locally with the following steps:
|
||||
|
||||
- Clone the Etcher repository.
|
||||
|
||||
```
|
||||
git clone https://github.com/resin-io/etcher
|
||||
```
|
||||
|
||||
- Install the dependencies by running:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
- Run the Etcher CLI from `bin/etcher`.
|
||||
|
||||
```
|
||||
./bin/etcher --help
|
||||
```
|
||||
|
||||
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
|
||||
```
|
@@ -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
|
||||
|
@@ -10,56 +10,91 @@ High-level Etcher overview
|
||||
Make sure you checkout our [ARCHITECTURE.md][ARCHITECTURE] guide, which aims to
|
||||
explain how all the pieces fit together.
|
||||
|
||||
Running locally
|
||||
---------------
|
||||
|
||||
See the [RUNNING-LOCALLY.md][RUNNING-LOCALLY] guide.
|
||||
|
||||
Developing
|
||||
----------
|
||||
|
||||
We rely on various `npm` scripts to perform some common tasks:
|
||||
### Prerequisites
|
||||
|
||||
- `npm run lint`: Run the linter.
|
||||
- `npm run sass`: Compile SCSS files.
|
||||
#### Common
|
||||
|
||||
We make use of [EditorConfig] to communicate indentation, line endings and
|
||||
other text editing default. We encourage you to install the relevant plugin in
|
||||
your text editor of choice to avoid having to fix any issues during the review
|
||||
process.
|
||||
- [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)
|
||||
|
||||
Updating a dependency
|
||||
---------------------
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Given we use [npm shrinkwrap][shrinkwrap], we have to take extra steps to make
|
||||
sure the `npm-shrinkwrap.json` file gets updated correctly when we update a
|
||||
dependency.
|
||||
You might need to run this with `sudo` or administrator permissions.
|
||||
|
||||
Use the following steps to ensure everything goes flawlessly:
|
||||
#### Windows
|
||||
|
||||
- Run `make electron-develop` to ensure you don't have extraneous dependencies
|
||||
you might have brought during development, or you are running older
|
||||
dependencies because you come from another branch or reference.
|
||||
- [NSIS v2.51](http://nsis.sourceforge.net/Main_Page) (v3.x won't work)
|
||||
- Either one of the following:
|
||||
- [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools) containing standalone compilers, libraries and scripts
|
||||
- Install the [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) via npm with `npm install --global windows-build-tools`
|
||||
- [Visual Studio Community 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48146) (free) (other editions, like Professional and Enterprise, should work too)
|
||||
**NOTE:** Visual Studio 2015 doesn't install C++ by default. You have to rerun the
|
||||
setup, select "Modify" and then check `Visual C++ -> Common Tools for Visual
|
||||
C++ 2015` (see http://stackoverflow.com/a/31955339)
|
||||
- [MinGW](http://www.mingw.org)
|
||||
|
||||
- Install the new version of the dependency. For example: `npm install --save
|
||||
<package>@<version>`. This will update the `npm-shrinkwrap.json` file.
|
||||
You might need to `npm config set msvs_version 2015` for node-gyp to correctly detect
|
||||
the version of Visual Studio you're using (in this example VS2015).
|
||||
|
||||
- Run `npm run clean-shrinkwrap`. This is a small script that ensures that
|
||||
operating system specific dependencies that could get included in the
|
||||
previous step are removed from `npm-shrinkwrap.json`.
|
||||
The following MinGW packages are required:
|
||||
|
||||
Some npm versions seem to contain an issue were all development dependencies
|
||||
will be included in `npm-shrinkwrap.json` when attempting to modify it (e.g: by
|
||||
`npm install`, `npm uninstall`, etc). A bulletproof way to ensure only the
|
||||
necessary dependencies get added is to run the following commands:
|
||||
- `msys-make`
|
||||
- `msys-unzip`
|
||||
- `msys-zip`
|
||||
- `msys-bash`
|
||||
- `msys-coreutils`
|
||||
|
||||
#### macOS
|
||||
|
||||
- [Xcode](https://developer.apple.com/xcode/)
|
||||
|
||||
It's not enough to have [Xcode Command Line Tools] installed. Xcode must be installed
|
||||
as well.
|
||||
|
||||
#### Linux
|
||||
|
||||
- `libudev-dev` for libusb (install with `sudo apt install libudev-dev` for example)
|
||||
|
||||
### Cloning the project
|
||||
|
||||
```sh
|
||||
git clone --recursive https://github.com/balena-io/etcher
|
||||
cd etcher
|
||||
```
|
||||
|
||||
### Installing npm dependencies
|
||||
|
||||
**NOTE:** Please make use of the following command to install npm dependencies rather
|
||||
than simply running `npm install` given that we need to do extra configuration
|
||||
to make sure native dependencies are correctly compiled for Electron, otherwise
|
||||
the application might not run successfully.
|
||||
|
||||
If you're on Windows, **run the command from the _Developer Command Prompt for
|
||||
VS2015_**, to ensure all Visual Studio command utilities are available in the
|
||||
`%PATH%`.
|
||||
|
||||
```sh
|
||||
make electron-develop
|
||||
npm prune --production
|
||||
npm shrinkwrap
|
||||
```
|
||||
|
||||
- Commit *both* `package.json` and `npm-shrinkwrap.json`.
|
||||
### Running the application
|
||||
|
||||
#### GUI
|
||||
|
||||
```sh
|
||||
# Build the GUI
|
||||
make webpack
|
||||
# Start Electron
|
||||
npm start
|
||||
```
|
||||
|
||||
Testing
|
||||
-------
|
||||
@@ -84,6 +119,62 @@ systems as they can before sending a pull request.
|
||||
*The test suite is run automatically by CI servers when you send a pull
|
||||
request.*
|
||||
|
||||
We also rely on various `make` targets to perform some common tasks:
|
||||
|
||||
- `make lint`: Run the linter.
|
||||
- `make sass`: Compile SCSS files.
|
||||
|
||||
We make use of [EditorConfig] to communicate indentation, line endings and
|
||||
other text editing default. We encourage you to install the relevant plugin in
|
||||
your text editor of choice to avoid having to fix any issues during the review
|
||||
process.
|
||||
|
||||
Updating a dependency
|
||||
---------------------
|
||||
|
||||
Given we use [npm shrinkwrap][shrinkwrap], we have to take extra steps to make
|
||||
sure the `npm-shrinkwrap.json` file gets updated correctly when we update a
|
||||
dependency.
|
||||
|
||||
Use the following steps to ensure everything goes flawlessly:
|
||||
|
||||
- Run `make electron-develop` to ensure you don't have extraneous dependencies
|
||||
you might have brought during development, or you are running older
|
||||
dependencies because you come from another branch or reference.
|
||||
|
||||
- Install the new version of the dependency. For example: `npm install --save
|
||||
<package>@<version>`. This will update the `npm-shrinkwrap.json` file.
|
||||
|
||||
- Commit *both* `package.json` and `npm-shrinkwrap.json`.
|
||||
|
||||
Diffing Binaries
|
||||
----------------
|
||||
|
||||
Binary files are tagged as "binary" in the `.gitattributes` file, but also have
|
||||
a `diff=hex` tag, which allows you to see hexdump-style diffs for binaries,
|
||||
if you add the following to either your global or repository-local git config:
|
||||
|
||||
```sh
|
||||
$ git config diff.hex.textconv hexdump
|
||||
$ git config diff.hex.binary true
|
||||
```
|
||||
|
||||
And global, respectively:
|
||||
|
||||
```sh
|
||||
$ git config --global diff.hex.textconv hexdump
|
||||
$ git config --global diff.hex.binary true
|
||||
```
|
||||
|
||||
If you don't have `hexdump` available on your platform,
|
||||
you can try [hxd], which is also a bit faster.
|
||||
|
||||
Commit Guidelines
|
||||
-----------------
|
||||
|
||||
See [COMMIT-GUIDELINES.md][COMMIT-GUIDELINES] for a thorough guide on how to
|
||||
write commit messages.
|
||||
|
||||
Sending a pull request
|
||||
----------------------
|
||||
|
||||
@@ -104,13 +195,13 @@ when your pull request is merged.
|
||||
|
||||
- Write a descriptive pull request title.
|
||||
|
||||
- Squash commits when possible, for example, when commiting review changes.
|
||||
- Squash commits when possible, for example, when committing review changes.
|
||||
|
||||
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.
|
||||
|
||||
@@ -119,7 +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
|
||||
[RUNNING-LOCALLY]: https://github.com/resin-io/etcher/blob/master/docs/RUNNING-LOCALLY.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
|
||||
|
@@ -1,46 +1,174 @@
|
||||
Maintaining Etcher
|
||||
==================
|
||||
|
||||
This document is meant to serve as a guide for maintainers to perform common
|
||||
tasks.
|
||||
This document is meant to serve as a guide for maintainers to perform common tasks.
|
||||
|
||||
Preparing a new version
|
||||
-----------------------
|
||||
Releasing
|
||||
---------
|
||||
|
||||
- Bump the version number in the `package.json`'s `version` property.
|
||||
### Release Types
|
||||
|
||||
- Bump the version number in the `package.json`'s `builder.win.version`
|
||||
- **snapshot** (default): A continues snapshot of current master, made by the CI services
|
||||
- **production**: Full releases
|
||||
|
||||
- Bump the version number in the `npm-shrinkwrap.json`'s `version` property.
|
||||
### Flight Plan
|
||||
|
||||
- Add a new entry to `CHANGELOG.md` by running `npm run changelog`.
|
||||
#### Preparation
|
||||
|
||||
- Re-take `screenshot.png` so it displays the latest version in the bottom
|
||||
right corner.
|
||||
- [Prepare the new version](#preparing-a-new-version)
|
||||
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
|
||||
- [Draft a release on GitHub](https://github.com/balena-io/etcher/releases)
|
||||
- Upload build artifacts to GitHub release draft
|
||||
|
||||
- Commit the changes with the version number as the commit title, including the
|
||||
`v` prefix, to `master`. For example:
|
||||
#### Testing
|
||||
|
||||
```sh
|
||||
git commit -m "v1.0.0" # not 1.0.0
|
||||
- Test the prepared release and build artifacts properly on **all supported operating systems** to prevent regressions that went uncaught by the CI tests (see [MANUAL-TESTING.md](MANUAL-TESTING.md))
|
||||
- If regressions or other issues arise, create issues on the repository for each one, and decide whether to fix them in this release (meaning repeating the process up until this point), or to follow up with a patch release
|
||||
|
||||
#### Publishing
|
||||
|
||||
- [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)
|
||||
- [Upload build artifacts to Amazon S3](#uploading-binaries-to-amazon-s3)
|
||||
- Post changelog with `#release-notes` tag on Flowdock
|
||||
- If this release packs noteworthy major changes:
|
||||
- Write a blog post about it, and / or
|
||||
- Write about it to the Etcher mailing list
|
||||
|
||||
### Generating binaries
|
||||
|
||||
**Environment**
|
||||
|
||||
Make sure to set the analytics tokens when generating production release binaries:
|
||||
|
||||
```bash
|
||||
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
|
||||
export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
||||
```
|
||||
|
||||
- Create an annotated tag for the new version. The commit title should equal
|
||||
the annotated tag name. For example:
|
||||
#### Linux
|
||||
|
||||
```sh
|
||||
git tag -a v1.0.0 -m "v1.0.0"
|
||||
##### Clean dist folder
|
||||
|
||||
**NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`)
|
||||
|
||||
```bash
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make distclean"
|
||||
```
|
||||
|
||||
- Push the commit and the annotated tag.
|
||||
##### Generating artifacts
|
||||
|
||||
```sh
|
||||
git push
|
||||
git push --tags
|
||||
```bash
|
||||
# x64
|
||||
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./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"
|
||||
|
||||
# x86
|
||||
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./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"
|
||||
```
|
||||
|
||||
Upgrading Electron
|
||||
------------------
|
||||
#### Mac OS
|
||||
|
||||
- Upgrade the `electron-prebuilt` dependency version in `package.json` to an
|
||||
*exact version* (no `~`, `^`, etc).
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_NAME` to generate signed binaries on Mac OS.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the zip
|
||||
make RELEASE_TYPE=production electron-installer-app-zip
|
||||
# Build the dmg
|
||||
make RELEASE_TYPE=production electron-installer-dmg
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows.
|
||||
|
||||
**NOTE:**
|
||||
- Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the Portable version
|
||||
make RELEASE_TYPE=production electron-installer-portable
|
||||
# Build the Installer
|
||||
make RELEASE_TYPE=production electron-installer-nsis
|
||||
```
|
||||
|
||||
### Uploading packages to Bintray
|
||||
|
||||
```bash
|
||||
export BINTRAY_USER="username@account"
|
||||
export BINTRAY_API_KEY="youruserapikey"
|
||||
```
|
||||
|
||||
```bash
|
||||
./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
|
||||
|
||||
```bash
|
||||
export S3_KEY="..."
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/publish/aws-s3.sh -b "balena-production-downloads" -v "1.2.1" -p "etcher" -f "dist/<filename>"
|
||||
```
|
||||
|
||||
### Dealing with a Problematic Release
|
||||
|
||||
There can be times where a release is accidentally plagued with bugs. If you
|
||||
released a new version and notice the error rates are higher than normal, then
|
||||
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 balena.io operations team to
|
||||
get write access to it.
|
||||
|
||||
The Etcher update notifier dialog and the website only show the a certain
|
||||
version if all the expected files have been uploaded to it, so deleting a
|
||||
single package or two is enough to bring down the whole version.
|
||||
|
||||
Use the following command to delete files from S3:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
115
docs/MANUAL-TESTING.md
Normal file
@@ -0,0 +1,115 @@
|
||||
Manual Testing
|
||||
==============
|
||||
|
||||
This document describes a high-level script of manual tests to check for. We
|
||||
should aim to replace items on this list with automated Spectron test cases.
|
||||
|
||||
Image Selection
|
||||
---------------
|
||||
|
||||
- [ ] Cancel image selection dialog
|
||||
- [ ] Select an unbootable image (without a partition table), and expect a
|
||||
sensible warning
|
||||
- [ ] Attempt to select a ZIP archive with more than one image
|
||||
- [ ] Attempt to select a tar archive (with any compression method)
|
||||
- [ ] Change image selection
|
||||
- [ ] Select a Windows image, and expect a sensible warning
|
||||
|
||||
Drive Selection
|
||||
---------------
|
||||
|
||||
- [ ] Open the drive selection modal
|
||||
- [ ] Switch drive selection
|
||||
- [ ] Insert a single drive, and expect auto-selection
|
||||
- [ ] Insert more than one drive, and don't expect auto-selection
|
||||
- [ ] Insert a locked SD Card and expect a warning
|
||||
- [ ] Insert a too small drive and expect a warning
|
||||
- [ ] Put an image into a drive and attempt to flash the image to the drive
|
||||
that contains it
|
||||
- [ ] Attempt to flash a compressed image (for which we can get the
|
||||
uncompressed size) into a drive that is big enough to hold the compressed
|
||||
image, but not big enough to hold the uncompressed version
|
||||
- [ ] Enable "Unsafe Mode" and attempt to select a system drive
|
||||
- [ ] Enable "Unsafe Mode", and if there is only one system drive (and no
|
||||
removable ones), don't expect autoselection
|
||||
|
||||
Image Support
|
||||
-------------
|
||||
|
||||
Run the following tests with and without validation enabled:
|
||||
|
||||
- [ ] Flash an uncompressed image
|
||||
- [ ] Flash a Bzip2 image
|
||||
- [ ] Flash a XZ image
|
||||
- [ ] Flash a ZIP image
|
||||
- [ ] Flash a GZ image
|
||||
- [ ] Flash a DMG image
|
||||
- [ ] Flash an image whose size is not a multiple of 512 bytes
|
||||
- [ ] Flash a compressed image whose size is not a multiple of 512 bytes
|
||||
- [ ] Flash an archive whose image size is not a multiple of 512 bytes
|
||||
- [ ] Flash an archive image containing a logo
|
||||
- [ ] Flash an archive image containing a blockmap file
|
||||
- [ ] Flash an archive image containing a manifest metadata file
|
||||
|
||||
Flashing Process
|
||||
----------------
|
||||
|
||||
- [ ] Unplug the drive during flash or validation
|
||||
- [ ] Click "Flash", cancel elevation dialog, and click "Flash" again
|
||||
- [ ] Start flashing an image, try to close Etcher, cancel the application
|
||||
close warning dialog, and check that Etcher continues to flash the image
|
||||
|
||||
### Child Writer
|
||||
|
||||
- [ ] Kill the child writer process (i.e. with `SIGINT` or `SIGKILL`), and
|
||||
check that the UI reacts appropriately
|
||||
- [ ] Close the application while flashing using the window manager close icon
|
||||
- [ ] Close the application while flashing using the OS keyboard shortcut
|
||||
- [ ] Close the application from the terminal using Ctrl-C while flashing
|
||||
- [ ] Force kill the application (using a process monitor tool, etc)
|
||||
|
||||
In all these cases, the child writer process should not remain alive. Note that
|
||||
in some systems you need to open your process monitor tool of choice with extra
|
||||
permissions to see the elevated child writer process.
|
||||
|
||||
GUI
|
||||
----
|
||||
|
||||
- [ ] Close application from the terminal using Ctrl-C while the application is
|
||||
idle
|
||||
- [ ] Click footer links that take you to an external website
|
||||
- [ ] Attempt to change image or drive selection while flashing
|
||||
- [ ] Go to the settings page while flashing and come back
|
||||
- [ ] Flash consecutive images without closing the application
|
||||
- [ ] Remove the selected drive right before clicking "Flash"
|
||||
- [ ] Minimize the application
|
||||
- [ ] Start the application given no internet connection
|
||||
|
||||
Success Banner
|
||||
--------------
|
||||
|
||||
- [ ] Click an external link on the success banner (with and without internet
|
||||
connection)
|
||||
|
||||
Elevation Prompt
|
||||
----------------
|
||||
|
||||
- [ ] Flash an image as `root`/administrator
|
||||
- [ ] Reject elevation prompt
|
||||
- [ ] Put incorrect elevation prompt password
|
||||
- [ ] Unplug the drive during elevation
|
||||
|
||||
Unmounting
|
||||
----------
|
||||
|
||||
- [ ] Disable unmounting and flash an image
|
||||
- [ ] Flash an image with a file system that is readable by the host OS, and
|
||||
check that is unmounted correctly
|
||||
|
||||
Analytics
|
||||
---------
|
||||
|
||||
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
|
||||
check that no request is sent
|
||||
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or
|
||||
F5), and check that initial events are not sent to Mixpanel**
|
@@ -4,13 +4,55 @@ Publishing Etcher
|
||||
This is a small guide to package and publish Etcher to all supported operating
|
||||
systems.
|
||||
|
||||
Release Types
|
||||
-------------
|
||||
|
||||
Etcher supports **production** and **snapshot** release types. Each is
|
||||
published to a different S3 bucket, and production release types are code
|
||||
signed, while snapshot release types aren't and include a short git commit-hash
|
||||
as a build number. For example, `1.0.0-beta.19` is a production release type,
|
||||
while `1.0.0-beta.19+531ab82` is a snapshot release type.
|
||||
|
||||
In terms of comparison: `1.0.0-beta.19` (production) < `1.0.0-beta.19+531ab82`
|
||||
(snapshot) < `1.0.0-rc.1` (production) < `1.0.0-rc.1+7fde24a` (snapshot) <
|
||||
`1.0.0` (production) < `1.0.0+2201e5f` (snapshot). Keep in mind that if you're
|
||||
running a production release type, you'll only be prompted to update to
|
||||
production release types, and if you're running a snapshot release type, you'll
|
||||
only be prompted to update to other snapshot release types.
|
||||
|
||||
The build system creates (and publishes) snapshot release types by default, but
|
||||
you can build a specific release type by setting the `RELEASE_TYPE` make
|
||||
variable. For example:
|
||||
|
||||
```sh
|
||||
make <target> RELEASE_TYPE=snapshot
|
||||
make <target> RELEASE_TYPE=production
|
||||
```
|
||||
|
||||
We can control the version range a specific Etcher version will consider when
|
||||
showing the update notification dialog by tweaking the `updates.semverRange`
|
||||
property of `package.json`.
|
||||
|
||||
Update Channels
|
||||
---------------
|
||||
|
||||
Etcher has a setting to include the unstable update channel. If this option is
|
||||
set, Etcher will consider both stable and unstable versions when showing the
|
||||
update notifier dialog. Unstable versions are the ones that contain a `beta`
|
||||
pre-release tag. For example:
|
||||
|
||||
- Production unstable version: `1.4.0-beta.1`
|
||||
- Snapshot unstable version: `1.4.0-beta.1+7fde24a`
|
||||
- Production stable version: `1.4.0`
|
||||
- Snapshot stable version: `1.4.0+7fde24a`
|
||||
|
||||
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.
|
||||
@@ -20,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
|
||||
@@ -29,40 +71,31 @@ employee by asking for it from the relevant people.
|
||||
Packaging
|
||||
---------
|
||||
|
||||
The resulting installers will be saved to `dist/out`.
|
||||
|
||||
Run the following commands:
|
||||
|
||||
### OS X
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
make electron-installer-dmg
|
||||
make electron-installer-app-zip
|
||||
```
|
||||
|
||||
The resulting installers will be saved to `release/out`.
|
||||
|
||||
### GNU/Linux
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
make electron-installer-appimage
|
||||
make electron-installer-debian
|
||||
```
|
||||
|
||||
The resulting installers will be saved to `release/out`.
|
||||
|
||||
### Windows
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
make electron-installer-zip
|
||||
make electron-installer-nsis
|
||||
```
|
||||
|
||||
The resulting installers will be saved to `etcher-release/installers`.
|
||||
|
||||
Publishing to Bintray
|
||||
---------------------
|
||||
|
||||
@@ -76,7 +109,7 @@ Make sure you set the following environment variables:
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
make publish-bintray-debian RELEASE_TYPE=<production|snapshot>
|
||||
make publish-bintray-debian
|
||||
```
|
||||
|
||||
Publishing to S3
|
||||
@@ -85,26 +118,17 @@ 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 downloads S3 bucket.
|
||||
access balena.io's production or snapshot S3 bucket.
|
||||
|
||||
> The publishing script only runs on UNIX based operating systems for now. You
|
||||
> can use something like [Cygwin][cygwin] to run it on Windows.
|
||||
|
||||
Run the following command to publish a specific file:
|
||||
Run the following command to publish all files for the current combination of
|
||||
_platform_ and _arch_ (building them if necessary):
|
||||
|
||||
```sh
|
||||
./scripts/publish/aws-s3.sh -f <file> -b <bucket> -v <version> -t <production|snapshot>
|
||||
```
|
||||
|
||||
Or run the following command to publish all files for the current combination
|
||||
of _platform_ and _arch_ (building them if necessary) :
|
||||
|
||||
```sh
|
||||
make publish-aws-s3 RELEASE_TYPE=<production|snapshot>
|
||||
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
|
||||
@@ -119,15 +143,12 @@ Publishing to Homebrew Cask
|
||||
Announcing
|
||||
----------
|
||||
|
||||
Post messages to the [Etcher forum][resin-forum-etcher] and
|
||||
[Etcher gitter channel][gitter-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
|
||||
[cygwin]: https://cygwin.com
|
||||
[bintray]: https://bintray.com
|
||||
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/etcher.rb
|
||||
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/balenaetcher.rb
|
||||
[homebrew-cask]: https://github.com/caskroom/homebrew-cask
|
||||
[resin-forum-etcher]: https://talk.resin.io/c/etcher/annoucements
|
||||
[gitter-etcher]: https://gitter.im/resin-io/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
|
||||
|
@@ -1,85 +0,0 @@
|
||||
Running locally
|
||||
===============
|
||||
|
||||
This document aims to serve as a guide to get Etcher running locally on your
|
||||
development machine.
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
### Common
|
||||
|
||||
- [NodeJS](https://nodejs.org) (at least v6)
|
||||
- [Bower](http://bower.io)
|
||||
- [UPX](http://upx.sourceforge.net)
|
||||
- [Python](https://www.python.org)
|
||||
- [SCSS Lint](https://github.com/brigade/scss-lint/)
|
||||
- [jq](https://stedolan.github.io/jq/)
|
||||
- [Asar](https://github.com/electron/asar)
|
||||
- [Codespell](https://github.com/lucasdemarchi/codespell)
|
||||
|
||||
### Windows
|
||||
|
||||
- [Rimraf](https://github.com/isaacs/rimraf)
|
||||
- [NSIS v2.51](http://nsis.sourceforge.net/Main_Page) (v3.x won't work)
|
||||
- [Visual Studio Community 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48146) (free) (other editions, like Professional and Enterprise, should work too)
|
||||
- Visual Studio 2015 doesn't install C++ by default. You have to rerun the
|
||||
setup, select Modify and then check `Visual C++ -> Common Tools for Visual
|
||||
C++ 2015` (see http://stackoverflow.com/a/31955339)
|
||||
- [MinGW](http://www.mingw.org)
|
||||
|
||||
The following MinGW packages are required:
|
||||
|
||||
- `msys-make`
|
||||
- `msys-unzip`
|
||||
- `msys-zip`
|
||||
- `msys-wget`
|
||||
- `msys-bash`
|
||||
- `msys-coreutils`
|
||||
|
||||
### OS X
|
||||
|
||||
- [XCode](https://developer.apple.com/xcode/)
|
||||
- [afsctool](https://brkirch.wordpress.com/afsctool/)
|
||||
|
||||
Cloning the project
|
||||
-------------------
|
||||
|
||||
```sh
|
||||
git clone https://github.com/resin-io/etcher
|
||||
cd etcher
|
||||
```
|
||||
|
||||
Installing npm dependencies
|
||||
---------------------------
|
||||
|
||||
**Make sure you have all the pre-requisites listed above installed in your
|
||||
system before running the `install` script.**
|
||||
|
||||
Please make use of the following scripts to install npm dependencies rather
|
||||
than simply running `npm install` given that we need to do extra configuration
|
||||
to make sure native dependencies are correctly compiled for Electron, otherwise
|
||||
the application might not run successfully.
|
||||
|
||||
If you're on Windows, **run the command from the _Developer Command Prompt for
|
||||
VS2015_**, to ensure all Visual Studio command utilities are available in the
|
||||
`%PATH%`.
|
||||
|
||||
```sh
|
||||
make electron-develop
|
||||
```
|
||||
|
||||
Running the application
|
||||
-----------------------
|
||||
|
||||
### GUI
|
||||
|
||||
```sh
|
||||
npm start
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```sh
|
||||
node bin/etcher
|
||||
```
|
@@ -14,8 +14,8 @@ images, usually available from the image publishers themselves.
|
||||
|
||||
Images known to require special treatment:
|
||||
|
||||
- Microsoft Windows (use [Windows USB/DVD Download Tool][windows-usb-tool], or
|
||||
[Rufus][rufus]).
|
||||
- Microsoft Windows (use [Windows USB/DVD Download Tool][windows-usb-tool],
|
||||
[Rufus][rufus], or [WoeUSB][woeusb]).
|
||||
|
||||
- Windows 10 IoT (use the [Windows 10 IoT Core Dashboard][windows-iot-dashboard])
|
||||
|
||||
@@ -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,24 +130,20 @@ run Etcher on a GNU/Linux system.
|
||||
|
||||
- liblzma (for xz decompression)
|
||||
|
||||
Disable update notifications
|
||||
----------------------------
|
||||
Simulate an update alert
|
||||
------------------------
|
||||
|
||||
You can disable update notifications, which can be useful when running Etcher
|
||||
outside a common desktop environment (like in a [resin.io] application), by
|
||||
setting the `ETCHER_DISABLE_UPDATES` environment variable.
|
||||
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.
|
||||
|
||||
In GNU/Linux and Mac OS X:
|
||||
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.
|
||||
|
||||
```sh
|
||||
export ETCHER_DISABLE_UPDATES=1
|
||||
```
|
||||
|
||||
In Windows:
|
||||
|
||||
```sh
|
||||
set ETCHER_DISABLE_UPDATES=1
|
||||
```
|
||||
See [`PUBLISHING.md`][publishing] for more details about release types.
|
||||
|
||||
Recovering broken drives
|
||||
------------------------
|
||||
@@ -210,20 +206,18 @@ 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
|
||||
[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
|
||||
[windows-iot-dashboard]: https://developer.microsoft.com/en-us/windows/iot/downloads
|
||||
[woeusb]: https://github.com/slacka/WoeUSB
|
||||
|
91
electron-builder.yml
Normal file
@@ -0,0 +1,91 @@
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2019 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: true
|
||||
publish: null
|
||||
files:
|
||||
- lib
|
||||
- lib/gui/app/index.html
|
||||
- generated
|
||||
- build/**/*.node
|
||||
- assets/icon.png
|
||||
- node_modules/**/*
|
||||
mac:
|
||||
icon: assets/icon.icns
|
||||
category: public.app-category.developer-tools
|
||||
dmg:
|
||||
background: assets/dmg/background.tiff
|
||||
icon: assets/icon.icns
|
||||
iconSize: 110
|
||||
contents:
|
||||
- x: 140
|
||||
y: 225
|
||||
- x: 415
|
||||
y: 225
|
||||
type: link
|
||||
path: /Applications
|
||||
window:
|
||||
width: 540
|
||||
height: 405
|
||||
win:
|
||||
icon: assets/icon.ico
|
||||
nsis:
|
||||
oneClick: true
|
||||
runAfterFinish: true
|
||||
installerIcon: assets/icon.ico
|
||||
uninstallerIcon: assets/icon.ico
|
||||
deleteAppDataOnUninstall: true
|
||||
license: LICENSE
|
||||
artifactName: "${productName}-Setup-${version}.${ext}"
|
||||
portable:
|
||||
artifactName: "${productName}-Portable-${version}.${ext}"
|
||||
requestExecutionLevel: user
|
||||
linux:
|
||||
category: Utility
|
||||
packageCategory: utils
|
||||
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
|
||||
depends:
|
||||
- gconf2
|
||||
- gconf-service
|
||||
- libappindicator1
|
||||
- libasound2
|
||||
- libatk1.0-0
|
||||
- libc6
|
||||
- libcairo2
|
||||
- libcups2
|
||||
- libdbus-1-3
|
||||
- libexpat1
|
||||
- libfontconfig1
|
||||
- libfreetype6
|
||||
- libgcc1
|
||||
- libgconf-2-4
|
||||
- libgdk-pixbuf2.0-0
|
||||
- libglib2.0-0
|
||||
- libgtk-3-0
|
||||
- liblzma5
|
||||
- libnotify4
|
||||
- libnspr4
|
||||
- libnss3
|
||||
- libpango1.0-0
|
||||
- libstdc++6
|
||||
- libx11-6
|
||||
- libxcomposite1
|
||||
- libxcursor1
|
||||
- libxdamage1
|
||||
- libxext6
|
||||
- libxfixes3
|
||||
- libxi6
|
||||
- libxrandr2
|
||||
- libxrender1
|
||||
- libxss1
|
||||
- libxtst6
|
||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||
rpm:
|
||||
depends:
|
||||
- lsb
|
||||
- libXScrnSaver
|
@@ -1,69 +0,0 @@
|
||||
Etcher Child Writer
|
||||
===================
|
||||
|
||||
This module is in charge of dealing with the gory details of elevating and
|
||||
managing the child writer process. As a word of warning, it contains tons of
|
||||
workarounds and "hacks" to deal with platform differences, packaging, and
|
||||
inter-process communication. This empowers us to write this small guide to
|
||||
explain how it works in a more high level manner, hoping to make it easier to
|
||||
grok for contributors.
|
||||
|
||||
The problem
|
||||
-----------
|
||||
|
||||
Elevating a forked process is an easy task. Thanks to the widely available NPM
|
||||
modules to display nice GUI prompt dialogs, elevation is just a matter of
|
||||
executing the process with one of those modules instead of with `child_process`
|
||||
directly.
|
||||
|
||||
The main problems we faced are:
|
||||
|
||||
- The modules that implement elevation provide "execution" support, but don't
|
||||
allow us to fork/spawn the process and consume its `stdout` and `stderr` in a
|
||||
stream fashion. This also means that we can't use the nice `process.send` IPC
|
||||
communication channel directly that `child_process.fork` gives us to send
|
||||
messages back to the parent.
|
||||
|
||||
- Since we can't assume anything from the environment Etcher is running on, we
|
||||
must make use of the same application entry point to execute both the GUI and
|
||||
the CLI code, which starts to get messy once we throw `asar` packaging into
|
||||
the mix.
|
||||
|
||||
- Each elevation mechanism has its quirks, mainly on GNU/Linux. Making sure
|
||||
that the forked process was elevated correctly and could work without issues
|
||||
required various workarounds targeting `pkexec` or `kdesudo`.
|
||||
|
||||
How it works
|
||||
------------
|
||||
|
||||
The Etcher binary runs in CLI or GUI mode depending on an environment variable
|
||||
called `ELECTRON_RUN_AS_NODE`. When this variable is set, it instructs Electron
|
||||
to run as a normal NodeJS process (without Chromium, etc), but still keep any
|
||||
patches applied by Electron, like `asar` support.
|
||||
|
||||
When the Etcher GUI is ran, and the user presses the "Flash!" button, the GUI
|
||||
creates an IPC server, and forks a process called the "writer proxy", passing
|
||||
it all the required information to perform the flashing, such as the image
|
||||
path, the device path, the current settings, etc.
|
||||
|
||||
The writer proxy then checks if its currently elevated, and if not, prompts the
|
||||
user for elevation and re-spawns itself.
|
||||
|
||||
Once the writer proxy has enough permissions to directly access devices, it
|
||||
spawns the Etcher CLI passing the `--robot` option along with all the
|
||||
information gathered before. The `--robot` option basically tells the Etcher
|
||||
CLI to output state information in a way that can be very easily parsed by the
|
||||
parent process.
|
||||
|
||||
The output of the Etcher CLI is then sent to the IPC server that was opened by
|
||||
the GUI, which nicely displays them in the progress bar the user sees.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
There are lots of details we're omitting for the sake of clarity. Feel free to
|
||||
dive in inside the child writer code, which is heavily commented to explain the
|
||||
reasons behind each decision or workaround.
|
||||
|
||||
Don't hesitate in getting in touch if you have any suggestion, or just want to
|
||||
know more!
|
@@ -1,99 +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');
|
||||
|
||||
/**
|
||||
* @summary Get the explicit boolean form of an argument
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @description
|
||||
* We refer as "explicit boolean form of an argument" to a boolean
|
||||
* argument in either normal or negated form.
|
||||
*
|
||||
* For example: `--check` and `--no-check`;
|
||||
*
|
||||
* @param {String} argumentName - argument name
|
||||
* @param {Boolean} value - argument value
|
||||
* @returns {String} argument
|
||||
*
|
||||
* @example
|
||||
* console.log(cli.getBooleanArgumentForm('check', true));
|
||||
* > '--check'
|
||||
*
|
||||
* @example
|
||||
* console.log(cli.getBooleanArgumentForm('check', false));
|
||||
* > '--no-check'
|
||||
*/
|
||||
exports.getBooleanArgumentForm = (argumentName, value) => {
|
||||
const prefix = _.attempt(() => {
|
||||
if (!value) {
|
||||
return '--no-';
|
||||
}
|
||||
|
||||
if (_.size(argumentName) === 1) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return '--';
|
||||
});
|
||||
|
||||
return prefix + argumentName;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get CLI writer arguments
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.image - image
|
||||
* @param {String} options.device - device
|
||||
* @param {String} options.entryPoint - entry point
|
||||
* @param {Boolean} [options.validateWriteOnSuccess] - validate write on success
|
||||
* @param {Boolean} [options.unmountOnSuccess] - unmount on success
|
||||
* @returns {String[]} arguments
|
||||
*
|
||||
* @example
|
||||
* const argv = cli.getArguments({
|
||||
* image: 'path/to/rpi.img',
|
||||
* device: '/dev/disk2'
|
||||
* entryPoint: 'path/to/app.asar',
|
||||
* validateWriteOnSuccess: true,
|
||||
* unmountOnSuccess: true
|
||||
* });
|
||||
*/
|
||||
exports.getArguments = (options) => {
|
||||
const argv = [
|
||||
options.entryPoint,
|
||||
options.image,
|
||||
'--drive',
|
||||
options.device,
|
||||
|
||||
// Explicitly set the boolean flag in positive
|
||||
// or negative way in order to be on the safe
|
||||
// side in case the Etcher CLI changes the
|
||||
// default value of these options.
|
||||
exports.getBooleanArgumentForm('unmount', options.unmountOnSuccess),
|
||||
exports.getBooleanArgumentForm('check', options.validateWriteOnSuccess)
|
||||
|
||||
];
|
||||
|
||||
return argv;
|
||||
};
|
@@ -1,156 +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 EventEmitter = require('events').EventEmitter;
|
||||
const _ = require('lodash');
|
||||
const childProcess = require('child_process');
|
||||
const ipc = require('node-ipc');
|
||||
const rendererUtils = require('./renderer-utils');
|
||||
const cli = require('./cli');
|
||||
const CONSTANTS = require('./constants');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
const robot = require('../shared/robot');
|
||||
|
||||
/**
|
||||
* @summary Perform a write
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} image - image
|
||||
* @param {Object} drive - drive
|
||||
* @param {Object} options - options
|
||||
* @returns {EventEmitter} event emitter
|
||||
*
|
||||
* @example
|
||||
* const child = childWriter.write('path/to/rpi.img', {
|
||||
* device: '/dev/disk2'
|
||||
* }, {
|
||||
* validateWriteOnSuccess: true,
|
||||
* unmountOnSuccess: true
|
||||
* });
|
||||
*
|
||||
* child.on('progress', (state) => {
|
||||
* console.log(state);
|
||||
* });
|
||||
*
|
||||
* child.on('error', (error) => {
|
||||
* throw error;
|
||||
* });
|
||||
*
|
||||
* child.on('done', () => {
|
||||
* console.log('Validation was successful!');
|
||||
* });
|
||||
*/
|
||||
exports.write = (image, drive, options) => {
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
const argv = cli.getArguments({
|
||||
entryPoint: rendererUtils.getApplicationEntryPoint(),
|
||||
image: image,
|
||||
device: drive.device,
|
||||
validateWriteOnSuccess: options.validateWriteOnSuccess,
|
||||
unmountOnSuccess: options.unmountOnSuccess
|
||||
});
|
||||
|
||||
// There might be multiple Etcher instances running at
|
||||
// the same time, therefore we must ensure each IPC
|
||||
// server/client has a different name.
|
||||
process.env.IPC_SERVER_ID = `etcher-server-${process.pid}`;
|
||||
process.env.IPC_CLIENT_ID = `etcher-client-${process.pid}`;
|
||||
|
||||
ipc.config.id = process.env.IPC_SERVER_ID;
|
||||
ipc.config.silent = true;
|
||||
ipc.serve();
|
||||
|
||||
const terminateServer = () => {
|
||||
|
||||
// Turns out we need to destroy all sockets for
|
||||
// the server to actually close. Otherwise, it
|
||||
// just stops receiving any further connections,
|
||||
// but remains open if there are active ones.
|
||||
_.each(ipc.server.sockets, (socket) => {
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
ipc.server.stop();
|
||||
};
|
||||
|
||||
const emitError = (error) => {
|
||||
terminateServer();
|
||||
emitter.emit('error', error);
|
||||
};
|
||||
|
||||
ipc.server.on('error', emitError);
|
||||
ipc.server.on('message', (data) => {
|
||||
let message;
|
||||
|
||||
try {
|
||||
message = robot.parseMessage(data);
|
||||
} catch (error) {
|
||||
return emitError(error);
|
||||
}
|
||||
|
||||
// The error object is decomposed by the CLI for serialisation
|
||||
// purposes. We compose it back to an `Error` here in order
|
||||
// to provide better encapsulation.
|
||||
if (robot.getCommand(message) === 'error') {
|
||||
return emitError(robot.recomposeErrorMessage(message));
|
||||
}
|
||||
|
||||
emitter.emit(robot.getCommand(message), robot.getData(message));
|
||||
});
|
||||
|
||||
ipc.server.on('start', () => {
|
||||
const child = childProcess.fork(CONSTANTS.WRITER_PROXY_SCRIPT, argv, {
|
||||
silent: true,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
console.info(`WRITER: ${data.toString()}`);
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
emitError(new Error(data.toString()));
|
||||
|
||||
// This function causes the `close` event to be emitted
|
||||
child.kill();
|
||||
|
||||
});
|
||||
|
||||
child.on('error', emitError);
|
||||
|
||||
child.on('close', (code) => {
|
||||
terminateServer();
|
||||
|
||||
if (code === EXIT_CODES.CANCELLED) {
|
||||
return emitter.emit('done', {
|
||||
cancelled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (code !== EXIT_CODES.SUCCESS && code !== EXIT_CODES.VALIDATION_ERROR) {
|
||||
return emitError(new Error(`Child process exited with error code: ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ipc.server.start();
|
||||
|
||||
return emitter;
|
||||
};
|
@@ -1,48 +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';
|
||||
|
||||
/**
|
||||
* This file is only meant to be loaded by the renderer process.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const isRunningInAsar = require('electron-is-running-in-asar');
|
||||
const electron = require('electron');
|
||||
const CONSTANTS = require('./constants');
|
||||
|
||||
/**
|
||||
* @summary Get application entry point
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} entry point
|
||||
*
|
||||
* @example
|
||||
* const entryPoint = rendererUtils.getApplicationEntryPoint();
|
||||
*/
|
||||
exports.getApplicationEntryPoint = () => {
|
||||
if (isRunningInAsar()) {
|
||||
return path.join(process.resourcesPath, 'app.asar');
|
||||
}
|
||||
|
||||
// On GNU/Linux, `pkexec` resolves relative paths
|
||||
// from `/root`, therefore we pass an absolute path,
|
||||
// in order to be on the safe side.
|
||||
return path.join(CONSTANTS.PROJECT_ROOT, electron.remote.process.argv[1]);
|
||||
|
||||
};
|
@@ -1,45 +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');
|
||||
|
||||
/**
|
||||
* @summary Split stringified object lines
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* This function takes special care to not consider new lines
|
||||
* inside the object properties.
|
||||
*
|
||||
* @param {String} lines - lines
|
||||
* @returns {String[]} split lines
|
||||
*
|
||||
* @example
|
||||
* const result = utils.splitObjectLines('{"foo":"bar"}\n{"hello":"Hello\nWorld"}');
|
||||
* console.log(result);
|
||||
*
|
||||
* > [ '{"foo":"bar"}', '{"hello":"Hello\nWorld"}' ]
|
||||
*/
|
||||
exports.splitObjectLines = (lines) => {
|
||||
return _.chain(lines)
|
||||
.split(/((?:[^\n"']|"[^"]*"|'[^']*')+)/)
|
||||
.map(_.trim)
|
||||
.reject(_.isEmpty)
|
||||
.value();
|
||||
};
|
@@ -1,194 +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 Bluebird = require('bluebird');
|
||||
const childProcess = require('child_process');
|
||||
const isElevated = Bluebird.promisify(require('is-elevated'));
|
||||
const ipc = require('node-ipc');
|
||||
const _ = require('lodash');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'));
|
||||
const utils = require('./utils');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
// This script is in charge of spawning the writer process and
|
||||
// ensuring it has the necessary privileges. It might look a bit
|
||||
// complex at first sight, but this is only because elevation
|
||||
// modules don't work in a spawn/fork fashion.
|
||||
//
|
||||
// This script spawns the writer process and redirects its `stdout`
|
||||
// and `stderr` to the parent process using IPC communication,
|
||||
// taking care of the writer elevation as needed.
|
||||
|
||||
const EXECUTABLE = process.argv[0];
|
||||
const ETCHER_ARGUMENTS = process.argv.slice(2);
|
||||
|
||||
return isElevated().then((elevated) => {
|
||||
|
||||
if (!elevated) {
|
||||
console.log('Attempting to elevate');
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
const elevator = Bluebird.promisifyAll(require('elevator'));
|
||||
|
||||
const commandArguments = [
|
||||
'set',
|
||||
'ELECTRON_RUN_AS_NODE=1',
|
||||
'&&',
|
||||
'set',
|
||||
`IPC_SERVER_ID=${process.env.IPC_SERVER_ID}`,
|
||||
'&&',
|
||||
'set',
|
||||
`IPC_CLIENT_ID=${process.env.IPC_CLIENT_ID}`,
|
||||
'&&',
|
||||
|
||||
// This is a trick to make the binary afterwards catch
|
||||
// the environment variables set just previously.
|
||||
'call'
|
||||
|
||||
].concat(process.argv);
|
||||
|
||||
// For debugging purposes
|
||||
console.log(`Running: ${commandArguments.join(' ')}`);
|
||||
|
||||
return elevator.executeAsync(commandArguments, {
|
||||
hidden: true,
|
||||
terminating: true,
|
||||
doNotPushdCurrentDirectory: true,
|
||||
waitForTermination: true
|
||||
}).catch({
|
||||
code: 'ELEVATE_CANCELLED'
|
||||
}, () => {
|
||||
process.exit(EXIT_CODES.CANCELLED);
|
||||
});
|
||||
}
|
||||
|
||||
const commandArguments = _.attempt(() => {
|
||||
const commandPrefix = [
|
||||
|
||||
// Some elevation tools, like `pkexec` or `kdesudo`, don't
|
||||
// provide a way to preserve the environment, therefore we
|
||||
// have to make sure the environment variables we're interested
|
||||
// in are manually inherited.
|
||||
'env',
|
||||
'ELECTRON_RUN_AS_NODE=1',
|
||||
`IPC_SERVER_ID=${process.env.IPC_SERVER_ID}`,
|
||||
`IPC_CLIENT_ID=${process.env.IPC_CLIENT_ID}`,
|
||||
|
||||
// This environment variable prevents the AppImages
|
||||
// desktop integration script from presenting the
|
||||
// "installation" dialog.
|
||||
'SKIP=1'
|
||||
|
||||
];
|
||||
|
||||
if (process.env.APPIMAGE && process.env.APPDIR) {
|
||||
|
||||
// Translate the current arguments to point to the AppImage
|
||||
// Relative paths are resolved from `/tmp/.mount_XXXXXX/usr`
|
||||
const translatedArguments = _.map(_.tail(process.argv), (argv) => {
|
||||
return argv.replace(path.join(process.env.APPDIR, 'usr/'), '');
|
||||
});
|
||||
|
||||
return commandPrefix
|
||||
.concat([ process.env.APPIMAGE ])
|
||||
.concat(translatedArguments);
|
||||
}
|
||||
|
||||
return commandPrefix.concat(process.argv);
|
||||
});
|
||||
|
||||
const command = _.join(_.map(commandArguments, (argument) => {
|
||||
return `"${argument.replace(/(")/g, '\\$1')}"`;
|
||||
}), ' ');
|
||||
|
||||
// For debugging purposes
|
||||
console.log(`Running: ${command}`);
|
||||
|
||||
return sudoPrompt.execAsync(command, {
|
||||
name: packageJSON.displayName
|
||||
}).then((stdout, stderr) => {
|
||||
if (!_.isEmpty(stderr)) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
}).catch({
|
||||
message: 'User did not grant permission.'
|
||||
}, () => {
|
||||
process.exit(EXIT_CODES.CANCELLED);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Re-spawning with elevation');
|
||||
|
||||
return new Bluebird((resolve, reject) => {
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID;
|
||||
ipc.config.silent = true;
|
||||
|
||||
// > If set to 0, the client will NOT try to reconnect.
|
||||
// See https://github.com/RIAEvangelist/node-ipc/
|
||||
//
|
||||
// The purpose behind this change is for this process
|
||||
// to emit a "disconnect" event as soon as the GUI
|
||||
// process is closed, so we can kill the CLI as well.
|
||||
ipc.config.stopRetrying = 0;
|
||||
|
||||
ipc.connectTo(process.env.IPC_SERVER_ID, () => {
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('error', reject);
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('connect', () => {
|
||||
|
||||
const child = childProcess.spawn(EXECUTABLE, ETCHER_ARGUMENTS, {
|
||||
env: {
|
||||
|
||||
// The CLI might call operating system utilities (like `diskutil`),
|
||||
// so we must ensure the `PATH` is inherited.
|
||||
PATH: process.env.PATH,
|
||||
|
||||
ELECTRON_RUN_AS_NODE: 1,
|
||||
ETCHER_CLI_ROBOT: 1
|
||||
}
|
||||
});
|
||||
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('disconnect', _.bind(child.kill, child));
|
||||
child.on('error', reject);
|
||||
child.on('close', resolve);
|
||||
|
||||
const emitMessage = (data) => {
|
||||
|
||||
// Output from stdout/stderr coming from the CLI might be buffered,
|
||||
// causing several progress lines to come up at once as single message.
|
||||
// Trying to parse multiple JSON objects separated by new lines will
|
||||
// of course make the parser confused, causing errors later on.
|
||||
_.each(utils.splitObjectLines(data.toString()), (object) => {
|
||||
ipc.of[process.env.IPC_SERVER_ID].emit('message', object);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
child.stdout.on('data', emitMessage);
|
||||
child.stderr.on('data', emitMessage);
|
||||
});
|
||||
});
|
||||
}).then((exitCode) => {
|
||||
process.exit(exitCode);
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR);
|
||||
});
|
@@ -1,46 +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. Consult the
|
||||
[`lib/child-writer`][child-writer] module to understand how elevation works on
|
||||
Etcher.
|
||||
|
||||
The robot option
|
||||
----------------
|
||||
|
||||
Setting the `ETCHER_CLI_ROBOT` environment variable allows other applications
|
||||
to easily consume the output of the Etcher CLI in real-time. When using the
|
||||
`ETCHER_CLI_ROBOT` option, the `--yes` option is implicit, therefore you need
|
||||
to manually specify `--drive`.
|
||||
|
||||
When `ETCHER_CLI_ROBOT` is used, the program will output JSON lines containing
|
||||
the progress state and other useful information. For example:
|
||||
|
||||
```
|
||||
$ sudo ETCHER_CLI_ROBOT=1 etcher image.iso --drive /dev/disk2
|
||||
{"command":"progress","data":{"type":"write","percentage":1,"eta":130,"speed":1703936}}
|
||||
...
|
||||
{"command":"progress","data":{"type":"check","percentage":100,"eta":0,"speed":17180514}}
|
||||
{"command":"done","data":{"sourceChecksum":"27c39a5d"}}
|
||||
```
|
||||
|
||||
See documentation about the robot mode at [`lib/shared/robot`][robot].
|
||||
|
||||
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
|
||||
[robot]: https://github.com/resin-io/etcher/tree/master/lib/shared/robot
|
||||
[child-writer]: https://github.com/resin-io/etcher/tree/master/lib/child-writer
|
@@ -1,130 +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 chalk = require('chalk');
|
||||
|
||||
/**
|
||||
* @summary Human-friendly error messages
|
||||
* @namespace HUMAN_FRIENDLY
|
||||
* @public
|
||||
*/
|
||||
exports.HUMAN_FRIENDLY = {
|
||||
|
||||
/* eslint-disable new-cap */
|
||||
|
||||
/**
|
||||
* @property {Function} ENOENT
|
||||
* @memberof HUMAN_FRIENDLY
|
||||
* @param {Error} error - error object
|
||||
* @returns {String} message
|
||||
*/
|
||||
ENOENT: (error) => {
|
||||
return `No such file or directory: ${error.path}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* @property {Function} EPERM
|
||||
* @memberof HUMAN_FRIENDLY
|
||||
* @returns {String} message
|
||||
*/
|
||||
EPERM: () => {
|
||||
return 'You\'re not authorized to perform this operation';
|
||||
},
|
||||
|
||||
/**
|
||||
* @property {Function} EACCES
|
||||
* @memberof HUMAN_FRIENDLY
|
||||
* @returns {String} message
|
||||
*/
|
||||
EACCES: () => {
|
||||
return 'You\'re don\'t have access to this resource';
|
||||
}
|
||||
|
||||
/* eslint-enable new-cap */
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get default error message
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @returns {String} error message
|
||||
*
|
||||
* @example
|
||||
* const message = defaultMessageGetter(new Error('foo bar'));
|
||||
* console.log(message);
|
||||
* > 'foo bar'
|
||||
*
|
||||
* @example
|
||||
* const message = defaultMessageGetter(new Error());
|
||||
* console.log(message);
|
||||
* > 'Unknown error'
|
||||
*/
|
||||
const defaultMessageGetter = (error) => {
|
||||
return error.message || 'Unknown error';
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get error message
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {(String|Error)} error - error
|
||||
* @returns {String} error message
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('Foo bar');
|
||||
* error.description = 'This is a fake error';
|
||||
*
|
||||
* console.log(errors.getErrorMessage(error));
|
||||
* > 'Foo bar\n\nThis is a fake error'
|
||||
*/
|
||||
exports.getErrorMessage = (error) => {
|
||||
if (_.isString(error)) {
|
||||
return exports.getErrorMessage(new Error(error));
|
||||
}
|
||||
|
||||
const message = _.attempt(() => {
|
||||
const title = _.get(exports.HUMAN_FRIENDLY, error.code, defaultMessageGetter)(error);
|
||||
return error.code ? `${error.code}: ${title}` : title;
|
||||
});
|
||||
|
||||
if (error.description) {
|
||||
return message + '\n\n' + error.description;
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Print an error to stderr
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {(Error|String)} error - error
|
||||
*
|
||||
* @example
|
||||
* errors.print(new Error('Oops!'));
|
||||
*/
|
||||
exports.print = (error) => {
|
||||
const message = exports.getErrorMessage(error);
|
||||
console.error(chalk.red(message));
|
||||
};
|
@@ -1,132 +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 isElevated = Bluebird.promisify(require('is-elevated'));
|
||||
const visuals = require('resin-cli-visuals');
|
||||
const form = require('resin-cli-form');
|
||||
const drivelist = Bluebird.promisifyAll(require('drivelist'));
|
||||
const writer = require('./writer');
|
||||
const errors = require('./errors');
|
||||
const options = require('./options');
|
||||
const robot = require('../shared/robot');
|
||||
const messages = require('../shared/messages');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
|
||||
isElevated().then((elevated) => {
|
||||
if (!elevated) {
|
||||
throw new Error(messages.error.elevationRequired());
|
||||
}
|
||||
|
||||
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 `undefined`,
|
||||
// otherwise the question will not be asked because
|
||||
// `false` is a defined value.
|
||||
yes: robot.isEnabled(process.env) || options.yes || undefined
|
||||
|
||||
}
|
||||
});
|
||||
}).then((answers) => {
|
||||
if (!answers.yes) {
|
||||
throw new Error('Aborted');
|
||||
}
|
||||
|
||||
const progressBars = {
|
||||
write: new visuals.Progress('Flashing'),
|
||||
check: new visuals.Progress('Validating')
|
||||
};
|
||||
|
||||
return drivelist.listAsync().then((drives) => {
|
||||
const selectedDrive = _.find(drives, {
|
||||
device: answers.drive
|
||||
});
|
||||
|
||||
if (!selectedDrive) {
|
||||
throw new Error(`Drive not found: ${answers.drive}`);
|
||||
}
|
||||
|
||||
return writer.writeImage(options._[0], selectedDrive, {
|
||||
unmountOnSuccess: options.unmount,
|
||||
validateWriteOnSuccess: options.check
|
||||
}, (state) => {
|
||||
|
||||
if (robot.isEnabled(process.env)) {
|
||||
robot.printMessage('progress', {
|
||||
type: state.type,
|
||||
percentage: Math.floor(state.percentage),
|
||||
eta: state.eta,
|
||||
speed: Math.floor(state.speed)
|
||||
});
|
||||
} else {
|
||||
progressBars[state.type].update(state);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
}).then((results) => {
|
||||
|
||||
return Bluebird.try(() => {
|
||||
if (robot.isEnabled(process.env)) {
|
||||
return robot.printMessage('done', {
|
||||
sourceChecksum: results.sourceChecksum
|
||||
});
|
||||
}
|
||||
|
||||
console.log(messages.info.flashComplete());
|
||||
|
||||
if (results.sourceChecksum) {
|
||||
console.log(`Checksum: ${results.sourceChecksum}`);
|
||||
}
|
||||
|
||||
}).then(() => {
|
||||
process.exit(EXIT_CODES.SUCCESS);
|
||||
});
|
||||
|
||||
}).catch((error) => {
|
||||
|
||||
return Bluebird.try(() => {
|
||||
if (robot.isEnabled(process.env)) {
|
||||
return robot.printError(error);
|
||||
}
|
||||
|
||||
errors.print(error);
|
||||
}).then(() => {
|
||||
if (error.code === 'EVALIDATION') {
|
||||
process.exit(EXIT_CODES.VALIDATION_ERROR);
|
||||
}
|
||||
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR);
|
||||
});
|
||||
|
||||
});
|
@@ -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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const yargs = require('yargs');
|
||||
const errors = require('./errors');
|
||||
const robot = require('../shared/robot');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
/**
|
||||
* @summary Parsed CLI options and arguments
|
||||
* @type Object
|
||||
* @public
|
||||
*/
|
||||
module.exports = yargs
|
||||
|
||||
// Don't wrap at all
|
||||
.wrap(null)
|
||||
|
||||
.demand(1, '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',
|
||||
' Gitter: https://gitter.im/resin-io/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(_.constant(packageJSON.version))
|
||||
|
||||
// Error reporting
|
||||
.fail(function(message, error) {
|
||||
if (robot.isEnabled(process.env)) {
|
||||
robot.printError(error || message);
|
||||
} else {
|
||||
yargs.showHelp();
|
||||
errors.print(error || message);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
})
|
||||
|
||||
// Assert that image exists
|
||||
.check((argv) => {
|
||||
fs.accessSync(argv._[0]);
|
||||
return true;
|
||||
})
|
||||
|
||||
.check((argv) => {
|
||||
if (robot.isEnabled(process.env) && !argv.drive) {
|
||||
throw new Error('Missing drive');
|
||||
}
|
||||
|
||||
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(2));
|
@@ -1,107 +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 childProcess = Bluebird.promisifyAll(require('child_process'));
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* @summary Unmount command templates
|
||||
* @namespace COMMAND_TEMPLATES
|
||||
* @private
|
||||
*
|
||||
* We make sure that the commands declared here exit
|
||||
* successfully even if the drive is not mounted.
|
||||
*/
|
||||
const COMMAND_TEMPLATES = {
|
||||
|
||||
/**
|
||||
* @property {String} darwin
|
||||
* @memberof COMMAND_TEMPLATES
|
||||
*/
|
||||
darwin: '/usr/sbin/diskutil unmountDisk force <%= device %>',
|
||||
|
||||
/**
|
||||
* @property {String} linux
|
||||
* @memberof COMMAND_TEMPLATES
|
||||
*
|
||||
* @description
|
||||
* If trying to unmount the raw device in Linux, we get:
|
||||
* > umount: /dev/sdN: not mounted
|
||||
* Therefore we use the ?* glob to make sure umount processes
|
||||
* the partitions of sdN independently (even if they contain multiple digits)
|
||||
* but not the raw device.
|
||||
* We also redirect stderr to /dev/null to ignore warnings
|
||||
* if a device is already unmounted.
|
||||
* Finally, we also wrap the command in a boolean expression
|
||||
* that always evaluates to true to ignore the return code,
|
||||
* which is non zero when a device was already unmounted.
|
||||
*/
|
||||
linux: 'umount <%= device %>?* 2>/dev/null || /bin/true'
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get UNIX unmount command
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} operatingSystem - operating system slug
|
||||
* @param {Object} drive - drive object
|
||||
* @returns {String} command
|
||||
*
|
||||
* @example
|
||||
* const drivelist = require('drivelist');
|
||||
* const os = require('os');
|
||||
*
|
||||
* drivelist.list((drives) => {
|
||||
* const command = unmount.getUNIXUnmountCommand(os.platform(), drives[0]);
|
||||
* });
|
||||
*/
|
||||
exports.getUNIXUnmountCommand = (operatingSystem, drive) => {
|
||||
return _.template(COMMAND_TEMPLATES[operatingSystem])(drive);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Unmount drive
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} drive - drive object
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* const Bluebird = require('bluebird');
|
||||
* const drivelist = Bluebird.promisifyAll(require('drivelist'));
|
||||
*
|
||||
* drivelist.listAsync().each(unmount.unmountDrive);
|
||||
*/
|
||||
exports.unmountDrive = (drive) => {
|
||||
const platform = os.platform();
|
||||
|
||||
if (platform === 'win32') {
|
||||
const removedrive = Bluebird.promisifyAll(require('removedrive'));
|
||||
return Bluebird.each(drive.mountpoints, (mountpoint) => {
|
||||
return removedrive.ejectAsync(mountpoint.path);
|
||||
});
|
||||
}
|
||||
|
||||
const command = exports.getUNIXUnmountCommand(platform, drive);
|
||||
return childProcess.execAsync(command);
|
||||
};
|
@@ -1,106 +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 imageWrite = require('etcher-image-write');
|
||||
const Bluebird = require('bluebird');
|
||||
const fs = Bluebird.promisifyAll(require('fs'));
|
||||
const os = require('os');
|
||||
const unmount = require('./unmount');
|
||||
const imageStream = require('../image-stream');
|
||||
|
||||
/**
|
||||
* @summary Write an image to a disk drive
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* See https://github.com/resin-io-modules/etcher-image-write for information
|
||||
* about the `state` object passed to `onProgress` callback.
|
||||
*
|
||||
* @param {String} imagePath - path to image
|
||||
* @param {Object} drive - drive
|
||||
* @param {Object} options - options
|
||||
* @param {Boolean} [options.unmountOnSuccess=false] - unmount on success
|
||||
* @param {Boolean} [options.validateWriteOnSuccess=false] - validate write on success
|
||||
* @param {Function} onProgress - on progress callback (state)
|
||||
*
|
||||
* @fulfil {Boolean} - whether the operation was successful
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* writer.writeImage('path/to/image.img', {
|
||||
* device: '/dev/disk2'
|
||||
* }, {
|
||||
* unmountOnSuccess: true,
|
||||
* validateWriteOnSuccess: true
|
||||
* }, (state) => {
|
||||
* console.log(state.percentage);
|
||||
* }).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.writeImage = (imagePath, drive, options, onProgress) => {
|
||||
return Bluebird.try(() => {
|
||||
|
||||
// Unmounting a drive in Windows means we can't write to it anymore
|
||||
if (os.platform() === 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
return unmount.unmountDrive(drive);
|
||||
}).then(() => {
|
||||
return fs.openAsync(drive.raw, 'rs+');
|
||||
}).then((driveFileDescriptor) => {
|
||||
return imageStream.getFromFilePath(imagePath).then((image) => {
|
||||
return imageWrite.write({
|
||||
fd: driveFileDescriptor,
|
||||
device: drive.raw,
|
||||
size: drive.size
|
||||
}, {
|
||||
stream: image.stream,
|
||||
size: image.size.original
|
||||
}, {
|
||||
check: options.validateWriteOnSuccess,
|
||||
transform: image.transform,
|
||||
bmap: image.bmap,
|
||||
bytesToZeroOutFromTheBeginning: image.bytesToZeroOutFromTheBeginning
|
||||
});
|
||||
}).then((writer) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
writer.on('progress', onProgress);
|
||||
writer.on('error', reject);
|
||||
writer.on('done', resolve);
|
||||
});
|
||||
}).tap(() => {
|
||||
|
||||
// Make sure the device stream file descriptor is closed
|
||||
// before returning control the the caller. Not closing
|
||||
// the file descriptor (and waiting for it) results in
|
||||
// `EBUSY` errors when attempting to unmount the drive
|
||||
// right afterwards in some Windows 7 systems.
|
||||
return fs.closeAsync(driveFileDescriptor).then(() => {
|
||||
|
||||
if (!options.unmountOnSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
return unmount.unmountDrive(drive);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
220
lib/gui/app.js
@@ -1,220 +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');
|
||||
const electron = require('electron');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
const messages = require('../shared/messages');
|
||||
|
||||
/* eslint-enable no-var */
|
||||
|
||||
const _ = require('lodash');
|
||||
const Store = require('./models/store');
|
||||
|
||||
const app = angular.module('Etcher', [
|
||||
require('angular-ui-router'),
|
||||
require('angular-ui-bootstrap'),
|
||||
require('angular-if-state'),
|
||||
|
||||
// Etcher modules
|
||||
require('./modules/analytics'),
|
||||
require('./modules/error'),
|
||||
require('./modules/drive-scanner'),
|
||||
|
||||
// Models
|
||||
require('./models/selection-state'),
|
||||
require('./models/flash-state'),
|
||||
require('./models/drives'),
|
||||
|
||||
// Components
|
||||
require('./components/svg-icon/svg-icon'),
|
||||
require('./components/update-notifier/update-notifier'),
|
||||
require('./components/drive-selector/drive-selector'),
|
||||
require('./components/warning-modal/warning-modal'),
|
||||
|
||||
// Pages
|
||||
require('./pages/main/main'),
|
||||
require('./pages/finish/finish'),
|
||||
require('./pages/settings/settings'),
|
||||
|
||||
// OS
|
||||
require('./os/window-progress/window-progress'),
|
||||
require('./os/open-external/open-external'),
|
||||
require('./os/dropzone/dropzone'),
|
||||
require('./os/dialog/dialog'),
|
||||
|
||||
// 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'
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateModel) => {
|
||||
AnalyticsService.logEvent('Application start');
|
||||
|
||||
if (UpdateNotifierService.shouldCheckForUpdates() && !process.env.ETCHER_DISABLE_UPDATES) {
|
||||
AnalyticsService.logEvent('Checking for updates');
|
||||
|
||||
UpdateNotifierService.isLatestVersion().then((isLatestVersion) => {
|
||||
|
||||
// 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 (!isLatestVersion && !SelectionStateModel.hasImage()) {
|
||||
|
||||
AnalyticsService.logEvent('Notifying update');
|
||||
return UpdateNotifierService.notify();
|
||||
}
|
||||
}).catch(ErrorService.reportException);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.run((AnalyticsService, OSWindowProgressService, FlashStateModel) => {
|
||||
Store.subscribe(() => {
|
||||
const flashState = FlashStateModel.getFlashState();
|
||||
|
||||
// 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.
|
||||
//
|
||||
// We use the presence of `.eta` to determine that the actual
|
||||
// writing started.
|
||||
if (!FlashStateModel.isFlashing() || !flashState.eta) {
|
||||
return;
|
||||
}
|
||||
|
||||
AnalyticsService.logDebug([
|
||||
`Progress (${flashState.type}):`,
|
||||
`${flashState.percentage}% at ${flashState.speed} MB/s`,
|
||||
`(eta ${flashState.eta}s)`
|
||||
].join(' '));
|
||||
|
||||
OSWindowProgressService.set(flashState.percentage);
|
||||
});
|
||||
});
|
||||
|
||||
app.run(($timeout, DriveScannerService, DrivesModel, ErrorService, DriveSelectorService) => {
|
||||
DriveScannerService.on('drives', (drives) => {
|
||||
|
||||
// Safely trigger a digest cycle.
|
||||
// In some cases, AngularJS doesn't aknowledge that the
|
||||
// available drives list has changed, and incorrectly
|
||||
// keeps asking the user to "Connect a drive".
|
||||
$timeout(() => {
|
||||
DrivesModel.setDrives(drives);
|
||||
});
|
||||
|
||||
if (_.isEmpty(drives)) {
|
||||
DriveSelectorService.close();
|
||||
}
|
||||
});
|
||||
|
||||
DriveScannerService.on('error', ErrorService.reportException);
|
||||
DriveScannerService.start();
|
||||
});
|
||||
|
||||
app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogService) => {
|
||||
let popupExists = false;
|
||||
|
||||
$window.addEventListener('beforeunload', (event) => {
|
||||
if (!FlashStateModel.isFlashing() || popupExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't close window while flashing
|
||||
event.returnValue = false;
|
||||
|
||||
// Don't open any more popups
|
||||
popupExists = true;
|
||||
|
||||
return OSDialogService.showWarning({
|
||||
confirmationLabel: 'Yes, quit',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: 'Are you sure you want to close Etcher?',
|
||||
description: messages.warning.exitWhileFlashing()
|
||||
}).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
|
||||
// This circumvents the 'beforeunload' event unlike
|
||||
// electron.remote.app.quit() which does not.
|
||||
electron.remote.process.exit(EXIT_CODES.SUCCESS);
|
||||
|
||||
}
|
||||
|
||||
popupExists = false;
|
||||
}).catch(ErrorService.reportException);
|
||||
});
|
||||
});
|
||||
|
||||
app.config(($urlRouterProvider) => {
|
||||
$urlRouterProvider.otherwise('/main');
|
||||
});
|
||||
|
||||
app.config(($provide) => {
|
||||
$provide.decorator('$exceptionHandler', ($delegate, $injector) => {
|
||||
return (exception, cause) => {
|
||||
const ErrorService = $injector.get('ErrorService');
|
||||
ErrorService.reportException(exception);
|
||||
$delegate(exception, cause);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
app.controller('HeaderController', function(SelectionStateModel, 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 = SelectionStateModel.getImageSupportUrl() || DEFAULT_SUPPORT_URL;
|
||||
OSOpenExternalService.open(supportUrl);
|
||||
};
|
||||
|
||||
});
|
539
lib/gui/app/app.js
Normal file
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* 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 sdk = require('etcher-sdk')
|
||||
const _ = require('lodash')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
|
||||
const EXIT_CODES = require('../../gui/app/modules/exit-codes')
|
||||
const messages = require('../../gui/app/modules/messages')
|
||||
const store = require('./models/store')
|
||||
const packageJSON = require('../../../package.json')
|
||||
const flashState = require('./models/flash-state')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('./models/settings')
|
||||
const windowProgress = require('./os/window-progress')
|
||||
const analytics = require('./modules/analytics')
|
||||
const availableDrives = require('./models/available-drives')
|
||||
const selectionState = require('./models/selection-state')
|
||||
const driveScanner = require('./modules/drive-scanner')
|
||||
const osDialog = require('./os/dialog')
|
||||
const exceptionReporter = require('./modules/exception-reporter')
|
||||
const updateLock = require('./modules/update-lock')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const screensaver = require('./modules/screensaver')
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method,lodash/prefer-get */
|
||||
|
||||
// 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
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
// Promise: event.reason
|
||||
// Bluebird: event.detail.reason
|
||||
// Anything else: event
|
||||
const error = event.reason || (event.detail && event.detail.reason) || event
|
||||
analytics.logException(error)
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
// Set application session UUID
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_APPLICATION_SESSION_UUID,
|
||||
data: uuidV4()
|
||||
})
|
||||
|
||||
// Set first flashing workflow UUID
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||
data: uuidV4()
|
||||
})
|
||||
|
||||
const applicationSessionUuid = store.getState().toJS().applicationSessionUuid
|
||||
const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid
|
||||
|
||||
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'),
|
||||
require('./components/file-selector'),
|
||||
|
||||
// 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@balena.io',
|
||||
'',
|
||||
`Version = ${packageJSON.version}, Type = ${packageJSON.packageType}`
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
app.run(() => {
|
||||
const currentVersion = packageJSON.version
|
||||
|
||||
analytics.logEvent('Application start', {
|
||||
packageType: packageJSON.packageType,
|
||||
version: currentVersion,
|
||||
applicationSessionUuid
|
||||
})
|
||||
})
|
||||
|
||||
app.run(() => {
|
||||
store.observe(() => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @summary The radix used by USB ID numbers
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const USB_ID_RADIX = 16
|
||||
|
||||
/**
|
||||
* @summary The expected length of a USB ID number
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const USB_ID_LENGTH = 4
|
||||
|
||||
/**
|
||||
* @summary Convert a USB id (e.g. product/vendor) to a string
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Number} id - USB id
|
||||
* @returns {String} string id
|
||||
*
|
||||
* @example
|
||||
* console.log(usbIdToString(2652))
|
||||
* > '0x0a5c'
|
||||
*/
|
||||
const usbIdToString = (id) => {
|
||||
return `0x${_.padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Product ID of BCM2708
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763
|
||||
|
||||
/**
|
||||
* @summary Product ID of BCM2710
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764
|
||||
|
||||
/**
|
||||
* @summary Compute module descriptions
|
||||
* @type {Object}
|
||||
* @constant
|
||||
*/
|
||||
const COMPUTE_MODULE_DESCRIPTIONS = {
|
||||
[USB_PRODUCT_ID_BCM2708_BOOT]: 'Compute Module 1',
|
||||
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3'
|
||||
}
|
||||
|
||||
app.run(($timeout) => {
|
||||
const BLACKLISTED_DRIVES = settings.has('driveBlacklist')
|
||||
? settings.get('driveBlacklist').split(',')
|
||||
: []
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const driveIsAllowed = (drive) => {
|
||||
return !(
|
||||
BLACKLISTED_DRIVES.includes(drive.devicePath) ||
|
||||
BLACKLISTED_DRIVES.includes(drive.device) ||
|
||||
BLACKLISTED_DRIVES.includes(drive.raw)
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc,consistent-return
|
||||
const prepareDrive = (drive) => {
|
||||
if (drive instanceof sdk.sourceDestination.BlockDevice) {
|
||||
return drive.drive
|
||||
} else if (drive instanceof sdk.sourceDestination.UsbbootDrive) {
|
||||
// This is a workaround etcher expecting a device string and a size
|
||||
drive.device = drive.usbDevice.portId
|
||||
drive.size = null
|
||||
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] || '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: [
|
||||
'Would you like to download the necessary drivers from the Raspberry Pi Foundation?',
|
||||
'This will open your browser.\n\n',
|
||||
'Once opened, download and run the installer from the "Windows Installer" section to install the drivers.'
|
||||
].join(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const setDrives = (drives) => {
|
||||
availableDrives.setDrives(_.values(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()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const getDrives = () => {
|
||||
return _.keyBy(availableDrives.getDrives() || [], 'device')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const addDrive = (drive) => {
|
||||
const preparedDrive = prepareDrive(drive)
|
||||
if (!driveIsAllowed(preparedDrive)) {
|
||||
return
|
||||
}
|
||||
const drives = getDrives()
|
||||
drives[preparedDrive.device] = preparedDrive
|
||||
setDrives(drives)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const removeDrive = (drive) => {
|
||||
const preparedDrive = prepareDrive(drive)
|
||||
const drives = getDrives()
|
||||
// eslint-disable-next-line prefer-reflect
|
||||
delete drives[preparedDrive.device]
|
||||
setDrives(drives)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const updateDriveProgress = (drive, progress) => {
|
||||
const drives = getDrives()
|
||||
const driveInMap = drives[drive.device]
|
||||
if (driveInMap) {
|
||||
driveInMap.progress = 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()
|
||||
})
|
||||
|
||||
app.run(($window) => {
|
||||
let popupExists = false
|
||||
|
||||
$window.addEventListener('beforeunload', (event) => {
|
||||
if (!flashState.isFlashing() || popupExists) {
|
||||
analytics.logEvent('Close application', {
|
||||
isFlashing: flashState.isFlashing(),
|
||||
applicationSessionUuid
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Don't close window while flashing
|
||||
event.returnValue = false
|
||||
|
||||
// Don't open any more popups
|
||||
popupExists = true
|
||||
|
||||
analytics.logEvent('Close attempt while flashing', { applicationSessionUuid, flashingWorkflowUuid })
|
||||
|
||||
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', {
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid
|
||||
})
|
||||
|
||||
// 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(exceptionReporter.report)
|
||||
})
|
||||
|
||||
/**
|
||||
* @summary Helper fn for events
|
||||
* @function
|
||||
* @private
|
||||
* @example
|
||||
* window.addEventListener('click', extendLock)
|
||||
*/
|
||||
const extendLock = () => {
|
||||
updateLock.extend()
|
||||
}
|
||||
|
||||
$window.addEventListener('click', extendLock)
|
||||
$window.addEventListener('touchstart', extendLock)
|
||||
|
||||
// Initial update lock acquisition
|
||||
extendLock()
|
||||
})
|
||||
|
||||
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,
|
||||
applicationSessionUuid
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
app.config(($urlRouterProvider) => {
|
||||
$urlRouterProvider.otherwise('/main')
|
||||
})
|
||||
|
||||
app.config(($provide) => {
|
||||
$provide.decorator('$exceptionHandler', ($delegate) => {
|
||||
return (exception, cause) => {
|
||||
exceptionReporter.report(exception)
|
||||
$delegate(exception, cause)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
app.config(($locationProvider) => {
|
||||
// NOTE(Shou): this seems to invoke a minor perf decrease when set to true
|
||||
$locationProvider.html5Mode({
|
||||
rewriteLinks: false
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Whether to show the help link
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* HeaderController.shouldShowHelp()
|
||||
*/
|
||||
this.shouldShowHelp = () => {
|
||||
return !settings.get('disableExternalLinks')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Whether to show the sleep button
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* HeaderController.shouldShowSleep()
|
||||
*/
|
||||
this.shouldShowSleep = () => {
|
||||
return settings.get('showScreensaverDelay')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Enables the screensaver
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* HeaderController.sleep()
|
||||
*/
|
||||
this.sleep = () => {
|
||||
screensaver.off()
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
screensaver.init()
|
32
lib/gui/app/components/confirm-modal/confirm-modal.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 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.ConfirmModal
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.ConfirmModal'
|
||||
const ConfirmModal = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal')
|
||||
])
|
||||
|
||||
ConfirmModal.controller('ConfirmModalController', require('./controllers/confirm-modal'))
|
||||
ConfirmModal.service('ConfirmModalService', require('./services/confirm-modal'))
|
||||
|
||||
module.exports = MODULE_NAME
|
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2018 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)
|
||||
}
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2018 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 show the confirm 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 confirm
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* ConfirmModalService.show({
|
||||
* description: 'Don\'t do this!',
|
||||
* confirmationLabel: 'Yes, continue!'
|
||||
* });
|
||||
*/
|
||||
this.show = (options = {}) => {
|
||||
options.description = $sce.trustAsHtml(options.description)
|
||||
return ModalService.open({
|
||||
name: 'confirm',
|
||||
template: require('../templates/confirm-modal.tpl.html'),
|
||||
controller: 'ConfirmModalController as modal',
|
||||
size: 'confirm-modal',
|
||||
resolve: {
|
||||
options: _.constant(options)
|
||||
}
|
||||
}).result
|
||||
}
|
||||
}
|
@@ -14,15 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
.modal-confirm-modal .modal-content {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.SVGIcon
|
||||
*/
|
||||
.modal-confirm-modal .modal-title .glyphicon {
|
||||
color: $palette-theme-danger-background;
|
||||
}
|
||||
|
||||
const angular = require('angular');
|
||||
const MODULE_NAME = 'Etcher.Components.SVGIcon';
|
||||
const SVGIcon = angular.module(MODULE_NAME, []);
|
||||
SVGIcon.directive('svgIcon', require('./directives/svg-icon'));
|
||||
|
||||
module.exports = MODULE_NAME;
|
||||
.modal-confirm-modal .modal-body {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<span>{{ ::modal.options.title }}</span>
|
||||
</h4>
|
||||
<button class="close"
|
||||
tabindex="11"
|
||||
ng-click="modal.reject()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>{{ ::modal.options.message }}</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="modal-menu">
|
||||
<button ng-if="modal.options.rejectionLabel" class="button button-block"
|
||||
tabindex="12"
|
||||
ng-class="{
|
||||
'button-default': modal.options.cancelButton === 'default',
|
||||
'button-primary': modal.options.cancelButton === 'primary',
|
||||
'button-warning': modal.options.cancelButton === 'warning',
|
||||
'button-danger': modal.options.cancelButton === 'danger',
|
||||
}"
|
||||
ng-click="modal.reject()">{{ ::modal.options.rejectionLabel }}</button>
|
||||
<button class="button button-block"
|
||||
tabindex="13"
|
||||
ng-class="{
|
||||
'button-default': modal.options.confirmButton === 'default',
|
||||
'button-primary': modal.options.confirmButton === 'primary',
|
||||
'button-warning': modal.options.confirmButton === 'warning',
|
||||
'button-danger': modal.options.confirmButton === 'danger',
|
||||
}"
|
||||
ng-click="modal.accept()">{{ ::modal.options.confirmationLabel }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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 Bluebird = require('bluebird')
|
||||
const constraints = require('../../../../../gui/app/modules/drive-constraints')
|
||||
const store = require('../../../models/store')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../../../gui/app/modules/utils')
|
||||
|
||||
module.exports = function (
|
||||
$q,
|
||||
$uibModalInstance,
|
||||
ConfirmModalService,
|
||||
OSOpenExternalService
|
||||
) {
|
||||
/**
|
||||
* @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),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
selectionState.toggleDrive(drive.device)
|
||||
}
|
||||
|
||||
return Bluebird.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Prompt the user to install missing usbboot drivers
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} drive - drive
|
||||
* @returns {Promise} - resolved promise
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorController.installMissingDrivers({
|
||||
* linkTitle: 'Go to example.com',
|
||||
* linkMessage: 'Examples are great, right?',
|
||||
* linkCTA: 'Call To Action',
|
||||
* link: 'https://example.com'
|
||||
* });
|
||||
*/
|
||||
this.installMissingDrivers = (drive) => {
|
||||
if (drive.link) {
|
||||
analytics.logEvent('Open driver link modal', {
|
||||
url: drive.link,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
return ConfirmModalService.show({
|
||||
confirmationLabel: 'Yes, continue',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: drive.linkTitle,
|
||||
confirmButton: 'primary',
|
||||
message: drive.linkMessage || `Etcher will open ${drive.link} in your browser`
|
||||
}).then((shouldContinue) => {
|
||||
if (shouldContinue) {
|
||||
OSOpenExternalService.open(drive.link)
|
||||
}
|
||||
}).catch((error) => {
|
||||
analytics.logException(error)
|
||||
})
|
||||
}
|
||||
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)', {
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,24 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.DriveSelector
|
||||
*/
|
||||
|
||||
const angular = require('angular');
|
||||
const MODULE_NAME = 'Etcher.Components.DriveSelector';
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.DriveSelector'
|
||||
const DriveSelector = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal'),
|
||||
require('../warning-modal/warning-modal'),
|
||||
require('../../models/drives'),
|
||||
require('../../models/selection-state'),
|
||||
require('../../models/drive-constraints'),
|
||||
require('../../utils/byte-size/byte-size')
|
||||
]);
|
||||
require('../confirm-modal/confirm-modal'),
|
||||
require('../../utils/byte-size/byte-size'),
|
||||
require('../../os/open-external/open-external')
|
||||
])
|
||||
|
||||
DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector'));
|
||||
DriveSelector.service('DriveSelectorService', require('./services/drive-selector'));
|
||||
DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector'))
|
||||
DriveSelector.service('DriveSelectorService', require('./services/drive-selector'))
|
||||
|
||||
module.exports = MODULE_NAME;
|
||||
module.exports = MODULE_NAME
|
32
lib/gui/app/components/drive-selector/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 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.Components.TargetSelector
|
||||
*/
|
||||
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.TargetSelector';
|
||||
const SelectTargetButton = angular.module(MODULE_NAME, []);
|
||||
|
||||
SelectTargetButton.component(
|
||||
'targetSelector',
|
||||
react2angular(require('./target-selector.jsx')),
|
||||
);
|
||||
|
||||
export = MODULE_NAME;
|
@@ -14,11 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
module.exports = function(ModalService, $q) {
|
||||
|
||||
let modal = null;
|
||||
module.exports = function (ModalService, $q) {
|
||||
let modal = null
|
||||
|
||||
/**
|
||||
* @summary Open the drive selector widget
|
||||
@@ -35,13 +34,14 @@ module.exports = function(ModalService, $q) {
|
||||
*/
|
||||
this.open = () => {
|
||||
modal = ModalService.open({
|
||||
template: './components/drive-selector/templates/drive-selector-modal.tpl.html',
|
||||
name: 'drive-selector',
|
||||
template: require('../templates/drive-selector-modal.tpl.html'),
|
||||
controller: 'DriveSelectorController as modal',
|
||||
size: 'drive-selector-modal'
|
||||
});
|
||||
})
|
||||
|
||||
return modal.result;
|
||||
};
|
||||
return modal.result
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Close the drive selector widget
|
||||
@@ -55,15 +55,12 @@ module.exports = function(ModalService, $q) {
|
||||
* DriveSelectorService.close();
|
||||
*/
|
||||
this.close = () => {
|
||||
|
||||
if (modal) {
|
||||
return modal.close();
|
||||
return modal.close()
|
||||
}
|
||||
|
||||
// Resolve `undefined` if the modal
|
||||
// was already closed for consistency
|
||||
return $q.resolve();
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
return $q.resolve()
|
||||
}
|
||||
}
|
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
.modal-drive-selector-modal .modal-content {
|
||||
width: 300px;
|
||||
width: 315px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
.modal-drive-selector-modal {
|
||||
|
||||
.list-group-item-footer {
|
||||
.list-group-item-footer:has(span) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@@ -52,12 +52,16 @@
|
||||
border-color: darken($palette-theme-light-background, 7%);
|
||||
padding: 12px 0;
|
||||
|
||||
> .tick {
|
||||
font-size: 11px;
|
||||
.list-group-item-section-expanded {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
:first-child {
|
||||
flex-grow: 1;
|
||||
.list-group-item-section + .list-group-item-section {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
> .tick {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@@ -67,6 +71,26 @@
|
||||
&[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 {
|
||||
@@ -78,5 +102,9 @@
|
||||
font-size: 11px;
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
}
|
||||
|
||||
.word-keep {
|
||||
word-break: keep-all;
|
||||
}
|
||||
}
|
||||
|
166
lib/gui/app/components/drive-selector/target-selector.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
'use strict'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const { default: styled } = require('styled-components')
|
||||
const {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
ThemedProvider
|
||||
} = require('./../../styled-components')
|
||||
const { Txt } = require('rendition')
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
const { bytesToClosestUnit } = require('./../../../../gui/app/modules/units')
|
||||
|
||||
const TargetDetail = styled((props) => (
|
||||
<Txt.span {...props}>
|
||||
</Txt.span>
|
||||
)) `
|
||||
float: ${({ float }) => float}
|
||||
`
|
||||
|
||||
const TargetDisplayText = ({
|
||||
description,
|
||||
size,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Txt.span {...props}>
|
||||
<TargetDetail
|
||||
float='left'>
|
||||
{description}
|
||||
</TargetDetail>
|
||||
<TargetDetail
|
||||
float='right'
|
||||
>
|
||||
{size}
|
||||
</TargetDetail>
|
||||
</Txt.span>
|
||||
)
|
||||
}
|
||||
|
||||
const TargetSelector = (props) => {
|
||||
const targets = props.selection.getSelectedDrives()
|
||||
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0]
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepNameButton
|
||||
plain
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
{/* eslint-disable no-magic-numbers */}
|
||||
{ middleEllipsis(target.description, 20) }
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={props.reselectDrive}
|
||||
>
|
||||
Change
|
||||
</ChangeButton>
|
||||
}
|
||||
<DetailsText>
|
||||
{ props.constraints.hasListDriveImageCompatibilityStatus(targets, props.image) &&
|
||||
<Txt.span className='glyphicon glyphicon-exclamation-sign'
|
||||
ml={2}
|
||||
tooltip={
|
||||
props.constraints.getListDriveImageCompatibilityStatuses(targets, props.image)[0].message
|
||||
}
|
||||
/>
|
||||
}
|
||||
{ bytesToClosestUnit(target.size) }
|
||||
</DetailsText>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
if (targets.length > 1) {
|
||||
const targetsTemplate = []
|
||||
for (const target of targets) {
|
||||
targetsTemplate.push((
|
||||
<DetailsText
|
||||
key={target.device}
|
||||
tooltip={
|
||||
`${target.description} ${target.displayName} ${bytesToClosestUnit(target.size)}`
|
||||
}
|
||||
px={21}
|
||||
>
|
||||
<TargetDisplayText
|
||||
description={middleEllipsis(target.description, 14)}
|
||||
size={bytesToClosestUnit(target.size)}
|
||||
>
|
||||
</TargetDisplayText>
|
||||
</DetailsText>
|
||||
))
|
||||
}
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepNameButton
|
||||
plain
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
{targets.length} Targets
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
onClick={props.reselectDrive}
|
||||
mb={14}
|
||||
>
|
||||
Change
|
||||
</ChangeButton>
|
||||
}
|
||||
{targetsTemplate}
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepButton
|
||||
tabindex={(targets.length > 0) ? -1 : 2 }
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
Select target
|
||||
</StepButton>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
TargetSelector.propTypes = {
|
||||
disabled: propTypes.bool,
|
||||
openDriveSelector: propTypes.func,
|
||||
selection: propTypes.object,
|
||||
reselectDrive: propTypes.func,
|
||||
flashing: propTypes.bool,
|
||||
constraints: propTypes.object,
|
||||
show: propTypes.bool,
|
||||
tooltip: propTypes.string
|
||||
}
|
||||
|
||||
module.exports = TargetSelector
|
@@ -0,0 +1,62 @@
|
||||
<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" ng-if="!drive.link">{{ drive.displayName }}</p>
|
||||
<p class="list-group-item-text" ng-if="drive.link">{{ drive.displayName }} - <b><a ng-click="modal.installMissingDrivers(drive)">{{ drive.linkCTA }}</a></b></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"
|
||||
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>
|
264
lib/gui/app/components/drive-selector2/drive-selector.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
import { Meter } from 'grommet';
|
||||
import { sortBy } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Badge, Modal, Table } from 'rendition';
|
||||
|
||||
import { getDrives } from '../../models/available-drives';
|
||||
import { COMPATIBILITY_STATUS_TYPES } from '../../modules/drive-constraints';
|
||||
import { subscribe } from '../../models/store';
|
||||
import { ThemedProvider } from '../../styled-components';
|
||||
import { bytesToClosestUnit } from '../../modules/units';
|
||||
|
||||
interface Drive {
|
||||
description: string;
|
||||
device: string;
|
||||
isSystem: boolean;
|
||||
isReadOnly: boolean;
|
||||
progress?: number;
|
||||
size?: number;
|
||||
link?: string;
|
||||
linkCTA?: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface CompatibilityStatus {
|
||||
type: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface DriveSelectorProps {
|
||||
title: string;
|
||||
close: () => void;
|
||||
setSelectedDrives: (drives: Drive[]) => void;
|
||||
isDriveSelected: (drive: Drive) => boolean;
|
||||
isDriveValid: (drive: Drive) => boolean;
|
||||
getDriveBadges: (drive: Drive) => CompatibilityStatus[];
|
||||
}
|
||||
|
||||
interface DriveSelectorState {
|
||||
drives: Drive[];
|
||||
selected: Drive[];
|
||||
disabledDrives: string[];
|
||||
}
|
||||
|
||||
const modalStyle = {
|
||||
width: '800px',
|
||||
height: '600px',
|
||||
paddingTop: '20px',
|
||||
paddingLeft: '30px',
|
||||
paddingRight: '30px',
|
||||
paddingBottom: '11px',
|
||||
};
|
||||
|
||||
const titleStyle = {
|
||||
color: '#2a506f',
|
||||
};
|
||||
|
||||
const subtitleStyle = {
|
||||
marginLeft: '10px',
|
||||
fontSize: '11px',
|
||||
color: '#5b82a7',
|
||||
};
|
||||
|
||||
const wrapperStyle = {
|
||||
height: '250px',
|
||||
overflowX: 'hidden' as 'hidden',
|
||||
overflowY: 'auto' as 'auto',
|
||||
};
|
||||
|
||||
export class DriveSelector2 extends React.Component<
|
||||
DriveSelectorProps,
|
||||
DriveSelectorState
|
||||
> {
|
||||
private table: Table<Drive> | null = null;
|
||||
private columns: {
|
||||
field: keyof Drive;
|
||||
label: string;
|
||||
render?: (value: any, row: Drive) => string | number | JSX.Element | null;
|
||||
}[];
|
||||
private unsubscribe?: () => void;
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
this.columns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
} as const,
|
||||
{
|
||||
field: 'size',
|
||||
label: 'Size',
|
||||
render: this.renderSize.bind(this),
|
||||
} as const,
|
||||
{
|
||||
field: 'displayName',
|
||||
label: 'Location',
|
||||
render: this.renderLocation.bind(this),
|
||||
} as const,
|
||||
{
|
||||
field: 'isReadOnly', // We don't use this, but a valid field that is not used in another column is required
|
||||
label: ' ',
|
||||
render: this.renderBadges.bind(this),
|
||||
} as const,
|
||||
];
|
||||
this.state = this.getNewState();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.update();
|
||||
if (this.unsubscribe === undefined) {
|
||||
this.unsubscribe = subscribe(this.update.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.unsubscribe !== undefined) {
|
||||
this.unsubscribe();
|
||||
this.unsubscribe = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getNewState() {
|
||||
let drives: Drive[] = getDrives();
|
||||
for (let i = 0; i < drives.length; i++) {
|
||||
drives[i] = { ...drives[i] };
|
||||
}
|
||||
drives = sortBy(drives, 'device');
|
||||
const selected = drives.filter(d => this.props.isDriveSelected(d));
|
||||
const disabledDrives = drives
|
||||
.filter(d => !this.props.isDriveValid(d))
|
||||
.map(d => d.device);
|
||||
return { drives, disabledDrives, selected };
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.setState(this.getNewState());
|
||||
this.updateTableSelection();
|
||||
}
|
||||
|
||||
private updateTableSelection() {
|
||||
if (this.table !== null) {
|
||||
this.table.setRowSelection(this.state.selected);
|
||||
}
|
||||
}
|
||||
|
||||
private renderSize(size: number) {
|
||||
if (size) {
|
||||
return bytesToClosestUnit(size);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private renderLocation(displayName: string, drive: Drive) {
|
||||
const result: Array<string | JSX.Element> = [displayName];
|
||||
if (drive.link && drive.linkCTA) {
|
||||
result.push(<a href={drive.link}>{drive.linkCTA}</a>);
|
||||
}
|
||||
return <React.Fragment>{result}</React.Fragment>;
|
||||
}
|
||||
|
||||
private renderBadges(_value: any, row: Drive) {
|
||||
const result = [];
|
||||
if (row.progress !== undefined) {
|
||||
result.push(
|
||||
<Meter
|
||||
size="small"
|
||||
thickness="xxsmall"
|
||||
values={[
|
||||
{
|
||||
value: row.progress,
|
||||
label: row.progress + '%',
|
||||
color: '#2297de',
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
result.push(
|
||||
...this.props.getDriveBadges(row).map((status: CompatibilityStatus) => {
|
||||
const props: {
|
||||
key: string;
|
||||
xsmall: true;
|
||||
danger?: boolean;
|
||||
warning?: boolean;
|
||||
} = { xsmall: true, key: status.message };
|
||||
if (status.type === COMPATIBILITY_STATUS_TYPES.ERROR) {
|
||||
props.danger = true;
|
||||
} else if (status.type === COMPATIBILITY_STATUS_TYPES.WARNING) {
|
||||
props.warning = true;
|
||||
}
|
||||
return <Badge {...props}>{status.message}</Badge>;
|
||||
}),
|
||||
);
|
||||
return <React.Fragment>{result}</React.Fragment>;
|
||||
}
|
||||
|
||||
private renderTbodyPrefix() {
|
||||
if (this.state.drives.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={this.columns.length} style={{ textAlign: 'center' }}>
|
||||
<b>Connect a drive</b>
|
||||
<div>No removable drive detected.</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<Modal
|
||||
titleElement={
|
||||
<div style={titleStyle}>
|
||||
{this.props.title}
|
||||
<span style={subtitleStyle}>
|
||||
{this.state.drives.length} found
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
action={`Select (${this.state.selected.length})`}
|
||||
style={modalStyle}
|
||||
done={this.props.close}
|
||||
>
|
||||
<div style={wrapperStyle}>
|
||||
<Table<Drive>
|
||||
ref={t => {
|
||||
this.table = t;
|
||||
this.updateTableSelection();
|
||||
}}
|
||||
rowKey="device"
|
||||
onCheck={this.onCheck.bind(this)}
|
||||
columns={this.columns}
|
||||
data={this.state.drives}
|
||||
disabledRows={this.state.disabledDrives}
|
||||
tbodyPrefix={this.renderTbodyPrefix()}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</ThemedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
private onCheck(checkedDrives: Drive[]): void {
|
||||
this.props.setSelectedDrives(checkedDrives);
|
||||
}
|
||||
}
|
38
lib/gui/app/components/drive-selector2/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
import { DriveSelector2 } from './drive-selector.tsx';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.DriveSelector2';
|
||||
|
||||
angular
|
||||
.module(MODULE_NAME, [])
|
||||
.component(
|
||||
'driveSelector2',
|
||||
react2angular(DriveSelector2, [
|
||||
'close',
|
||||
'getDriveBadges',
|
||||
'isDriveSelected',
|
||||
'isDriveValid',
|
||||
'setSelectedDrives',
|
||||
'title',
|
||||
]),
|
||||
);
|
||||
|
||||
export = MODULE_NAME;
|
57
lib/gui/app/components/featured-project/featured-project.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const SafeWebview = require('../safe-webview/safe-webview.jsx')
|
||||
const settings = require('../../models/settings')
|
||||
const analytics = require('../../modules/analytics')
|
||||
|
||||
class FeaturedProject extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
endpoint: null
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
return settings.load()
|
||||
.then(() => {
|
||||
const endpoint = settings.get('featuredProjectEndpoint') || 'https://assets.balena.io/etcher-featured/index.html'
|
||||
this.setState({ endpoint })
|
||||
})
|
||||
.catch(analytics.logException)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (this.state.endpoint) ? (
|
||||
<SafeWebview
|
||||
src={this.state.endpoint}
|
||||
{...this.props}>
|
||||
</SafeWebview>
|
||||
) : null
|
||||
}
|
||||
}
|
||||
|
||||
FeaturedProject.propTypes = {
|
||||
onWebviewShow: propTypes.func
|
||||
}
|
||||
|
||||
module.exports = FeaturedProject
|
34
lib/gui/app/components/featured-project/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.FeaturedProject
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FeaturedProject'
|
||||
const FeaturedProject = angular.module(MODULE_NAME, [])
|
||||
|
||||
FeaturedProject.component(
|
||||
'featuredProject',
|
||||
react2angular(require('./featured-project.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2018 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 os = require('os')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../../../gui/app/modules/utils')
|
||||
const angular = require('angular')
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method */
|
||||
|
||||
module.exports = function (
|
||||
$uibModalInstance
|
||||
) {
|
||||
/**
|
||||
* @summary Close the modal
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* FileSelectorController.close();
|
||||
*/
|
||||
this.close = () => {
|
||||
$uibModalInstance.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Folder to constrain the file picker to
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - folder to constrain by
|
||||
*
|
||||
* @example
|
||||
* FileSelectorController.getFolderConstraint()
|
||||
*/
|
||||
this.getFolderConstraint = utils.memoize(() => {
|
||||
return settings.has('fileBrowserConstraintPath')
|
||||
? settings.get('fileBrowserConstraintPath')
|
||||
: ''
|
||||
}, angular.equals)
|
||||
|
||||
/**
|
||||
* @summary Get initial path
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - path
|
||||
*
|
||||
* @example
|
||||
* <file-selector path="FileSelectorController.getPath()"></file-selector>
|
||||
*/
|
||||
this.getPath = () => {
|
||||
const constraintFolderPath = this.getFolderConstraint()
|
||||
return _.isEmpty(constraintFolderPath) ? os.homedir() : constraintFolderPath
|
||||
}
|
||||
}
|
45
lib/gui/app/components/file-selector/file-selector/colors.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2018 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 Color scheme
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const colors = {
|
||||
primary: {
|
||||
color: '#3a3c41',
|
||||
background: '#ffffff',
|
||||
subColor: '#ababab',
|
||||
faded: '#c3c4c6'
|
||||
},
|
||||
secondary: {
|
||||
color: '#1c1d1e',
|
||||
background: '#ebeff4',
|
||||
title: '#b3b6b9'
|
||||
},
|
||||
highlight: {
|
||||
color: 'white',
|
||||
background: '#2297de'
|
||||
},
|
||||
soft: {
|
||||
color: '#4d5056'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = colors
|
321
lib/gui/app/components/file-selector/file-selector/file-list.jsx
Normal file
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* Copyright 2018 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 React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const rendition = require('rendition')
|
||||
const colors = require('./colors')
|
||||
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const files = require('../../../models/files')
|
||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
||||
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
|
||||
|
||||
const debug = require('debug')('etcher:gui:file-selector')
|
||||
|
||||
/**
|
||||
* @summary Character limit of a filename before a middle-ellipsis is added
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const FILENAME_CHAR_LIMIT = 20
|
||||
|
||||
/**
|
||||
* @summary Pattern to match all supported formats for highlighting
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const SUPPORTED_FORMATS_PATTERN = new RegExp(`^\\.(${supportedFormats.getAllExtensions().join('|')})$`, 'i')
|
||||
|
||||
/**
|
||||
* @summary Flex styled component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
flex: ${ props => props.flex };
|
||||
flex-direction: ${ props => props.direction };
|
||||
justify-content: ${ props => props.justifyContent };
|
||||
align-items: ${ props => props.alignItems };
|
||||
flex-wrap: ${ props => props.wrap };
|
||||
flex-grow: ${ props => props.grow };
|
||||
`
|
||||
|
||||
/**
|
||||
* @summary Anchor flex styled component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const ClickableFlex = styled.a`
|
||||
display: flex;
|
||||
flex: ${ props => props.flex };
|
||||
flex-direction: ${ props => props.direction };
|
||||
justify-content: ${ props => props.justifyContent };
|
||||
align-items: ${ props => props.alignItems };
|
||||
flex-wrap: ${ props => props.wrap };
|
||||
flex-grow: ${ props => props.grow };
|
||||
`
|
||||
|
||||
/**
|
||||
* @summary FileList scroll wrapper element
|
||||
* @class
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
class UnstyledFileListWrap extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.scrollElem = null
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Flex className={ this.props.className }
|
||||
ref={ ::this.setScrollElem }
|
||||
wrap="wrap">
|
||||
{ this.props.children }
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
setScrollElem (element) {
|
||||
this.scrollElem = element
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.scrollElem) {
|
||||
this.scrollElem.scrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary FileList scroll wrapper element
|
||||
* @class
|
||||
* @type {StyledComponent}
|
||||
*/
|
||||
const FileListWrap = styled(UnstyledFileListWrap)`
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
/**
|
||||
* @summary File element
|
||||
* @class
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
class UnstyledFile extends React.PureComponent {
|
||||
|
||||
static getFileIconClass (file) {
|
||||
return file.isDirectory
|
||||
? 'fas fa-folder'
|
||||
: 'fas fa-file-alt'
|
||||
}
|
||||
|
||||
onHighlight (event) {
|
||||
event.preventDefault()
|
||||
this.props.onHighlight(this.props.file)
|
||||
}
|
||||
|
||||
onSelect (event) {
|
||||
event.preventDefault()
|
||||
this.props.onSelect(this.props.file)
|
||||
}
|
||||
|
||||
render () {
|
||||
const file = this.props.file
|
||||
return (
|
||||
<ClickableFlex
|
||||
data-path={ file.path }
|
||||
href={ `file://${file.path}` }
|
||||
direction="column"
|
||||
alignItems="stretch"
|
||||
className={ this.props.className }
|
||||
onClick={ ::this.onHighlight }
|
||||
onDoubleClick={ ::this.onSelect }>
|
||||
<span className={ UnstyledFile.getFileIconClass(file) } />
|
||||
<span>{ middleEllipsis(file.basename, FILENAME_CHAR_LIMIT) }</span>
|
||||
<div>{ file.isDirectory ? '' : prettyBytes(file.size || 0) }</div>
|
||||
</ClickableFlex>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary File element
|
||||
* @class
|
||||
* @type {StyledComponent}
|
||||
*/
|
||||
const File = styled(UnstyledFile)`
|
||||
width: 100px;
|
||||
min-height: 100px;
|
||||
max-height: 128px;
|
||||
margin: 5px 10px;
|
||||
padding: 5px;
|
||||
background-color: none;
|
||||
transition: 0.05s background-color ease-out;
|
||||
color: ${ colors.primary.color };
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
word-break: break-word;
|
||||
|
||||
> span:first-of-type {
|
||||
align-self: center;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
font-size: 48px;
|
||||
color: ${ props => props.disabled ? colors.primary.faded : colors.soft.color };
|
||||
}
|
||||
|
||||
> span:last-of-type {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
background-color: none;
|
||||
color: ${ colors.primary.subColor };
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:hover, :visited {
|
||||
color: ${ colors.primary.color };
|
||||
}
|
||||
|
||||
:focus,
|
||||
:active {
|
||||
color: ${ colors.highlight.color };
|
||||
background-color: ${ colors.highlight.background };
|
||||
}
|
||||
|
||||
:focus > span:first-of-type,
|
||||
:active > span:first-of-type {
|
||||
color: ${ colors.highlight.color };
|
||||
}
|
||||
|
||||
:focus > div:last-child,
|
||||
:active > div:last-child {
|
||||
color: ${ colors.highlight.color };
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* @summary FileList element
|
||||
* @class
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
class FileList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
path: props.path,
|
||||
highlighted: null,
|
||||
files: [],
|
||||
}
|
||||
|
||||
debug('FileList', props)
|
||||
}
|
||||
|
||||
readdir (dirname) {
|
||||
debug('FileList:readdir', dirname)
|
||||
|
||||
if (this.props.constraintPath && dirname === '/') {
|
||||
if (this.props.constraint) {
|
||||
const mountpoints = this.props.constraint.mountpoints.map(( mount ) => {
|
||||
const entry = new files.FileEntry(mount.path, {
|
||||
size: 0,
|
||||
isFile: () => false,
|
||||
isDirectory: () => true
|
||||
})
|
||||
entry.name = mount.label
|
||||
return entry
|
||||
})
|
||||
debug('FileList:readdir', mountpoints)
|
||||
window.requestAnimationFrame(() => {
|
||||
this.setState({ files: mountpoints })
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
files.readdirAsync(dirname).then((files) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.setState({ files: files })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
process.nextTick(() => {
|
||||
this.readdir(this.state.path)
|
||||
})
|
||||
}
|
||||
|
||||
onHighlight (file) {
|
||||
debug('FileList:onHighlight', file)
|
||||
this.props.onHighlight(file)
|
||||
}
|
||||
|
||||
onSelect (file) {
|
||||
debug('FileList:onSelect', file.path, file.isDirectory)
|
||||
this.props.onSelect(file)
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
const shouldUpdate = (this.state.files !== nextState.files)
|
||||
debug('FileList:shouldComponentUpdate', shouldUpdate)
|
||||
if (this.props.path !== nextProps.path || this.props.constraint !== nextProps.constraint) {
|
||||
process.nextTick(() => {
|
||||
this.readdir(nextProps.path)
|
||||
})
|
||||
}
|
||||
return shouldUpdate
|
||||
}
|
||||
|
||||
static isSelectable (file) {
|
||||
return file.isDirectory || !file.ext ||
|
||||
SUPPORTED_FORMATS_PATTERN.test(file.ext)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<FileListWrap wrap="wrap">
|
||||
{
|
||||
this.state.files.map((file) => {
|
||||
return (
|
||||
<File key={ file.path }
|
||||
file={ file }
|
||||
disabled={ !FileList.isSelectable(file) }
|
||||
onSelect={ ::this.onSelect }
|
||||
onHighlight={ ::this.onHighlight }/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</FileListWrap>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileList
|
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
* Copyright 2018 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 path = require('path')
|
||||
const sdk = require('etcher-sdk')
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const rendition = require('rendition')
|
||||
const colors = require('./colors')
|
||||
|
||||
const Breadcrumbs = require('./path-breadcrumbs')
|
||||
const FileList = require('./file-list')
|
||||
const RecentFiles = require('./recent-files')
|
||||
const files = require('../../../models/files')
|
||||
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const store = require('../../../models/store')
|
||||
const osDialog = require('../../../os/dialog')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
const errors = require('../../../../../gui/app/modules/errors')
|
||||
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
|
||||
const debug = require('debug')('etcher:gui:file-selector')
|
||||
|
||||
/**
|
||||
* @summary Flex styled component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
flex: ${ props => props.flex };
|
||||
flex-direction: ${ props => props.direction };
|
||||
justify-content: ${ props => props.justifyContent };
|
||||
align-items: ${ props => props.alignItems };
|
||||
flex-wrap: ${ props => props.wrap };
|
||||
flex-grow: ${ props => props.grow };
|
||||
overflow: ${ props => props.overflow };
|
||||
`
|
||||
|
||||
const Header = styled(Flex) `
|
||||
padding: 10px 15px 0;
|
||||
border-bottom: 1px solid ${ colors.primary.faded };
|
||||
|
||||
> * {
|
||||
margin: 5px;
|
||||
}
|
||||
`
|
||||
|
||||
const Main = styled(Flex) ``
|
||||
|
||||
const Footer = styled(Flex) `
|
||||
padding: 10px;
|
||||
flex: 0 0 auto;
|
||||
border-top: 1px solid ${ colors.primary.faded };
|
||||
|
||||
> * {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
> button {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`
|
||||
|
||||
class UnstyledFilePath extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<div className={ this.props.className }>
|
||||
<span>{
|
||||
this.props.file && !this.props.file.isDirectory
|
||||
? this.props.file.basename
|
||||
: ''
|
||||
}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const FilePath = styled(UnstyledFilePath)`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
> span {
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`
|
||||
|
||||
class FileSelector extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
path: props.path,
|
||||
highlighted: null,
|
||||
constraint: null,
|
||||
files: [],
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.constraintpath) {
|
||||
const device = files.getConstraintDevice(this.props.constraintpath)
|
||||
debug('FileSelector:getConstraintDevice', device)
|
||||
if (device !== undefined) {
|
||||
this.setState({ constraint: device.drive })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
confirmSelection () {
|
||||
if (this.state.highlighted) {
|
||||
this.selectFile(this.state.highlighted)
|
||||
}
|
||||
}
|
||||
|
||||
close () {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
debug('FileSelector:componentDidUpdate')
|
||||
}
|
||||
|
||||
containPath (newPath) {
|
||||
if (this.state.constraint) {
|
||||
const isContained = this.state.constraint.mountpoints.some((mount) => {
|
||||
return !path.relative(mount.path, newPath).startsWith('..')
|
||||
})
|
||||
if (!isContained) {
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
|
||||
navigate (newPath) {
|
||||
debug('FileSelector:navigate', newPath)
|
||||
this.setState({ path: this.containPath(newPath) })
|
||||
}
|
||||
|
||||
navigateUp () {
|
||||
let newPath = this.containPath(path.join(this.state.path, '..'))
|
||||
debug('FileSelector:navigateUp', this.state.path, '->', newPath)
|
||||
this.setState({ path: newPath })
|
||||
}
|
||||
|
||||
selectImage (image) {
|
||||
debug('FileSelector:selectImage', image)
|
||||
|
||||
if (!supportedFormats.isSupportedImage(image.path)) {
|
||||
const invalidImageError = errors.createUserError({
|
||||
title: 'Invalid image',
|
||||
description: messages.error.invalidImage(image.path)
|
||||
})
|
||||
|
||||
osDialog.showError(invalidImageError)
|
||||
analytics.logEvent('Invalid image', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
return Bluebird.try(() => {
|
||||
let message = null
|
||||
|
||||
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
||||
analytics.logEvent('Possibly Windows image', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
message = messages.warning.looksLikeWindowsImage()
|
||||
} else if (!image.hasMBR) {
|
||||
analytics.logEvent('Missing partition table', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
message = messages.warning.missingPartitionTable()
|
||||
}
|
||||
|
||||
if (message) {
|
||||
// TODO: `Continue` should be on a red background (dangerous action) instead of `Change`.
|
||||
// We want `X` to act as `Continue`, that's why `Continue` is the `rejectionLabel`
|
||||
return osDialog.showWarning({
|
||||
confirmationLabel: 'Change',
|
||||
rejectionLabel: 'Continue',
|
||||
title: 'Warning',
|
||||
description: message
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}).then((shouldChange) => {
|
||||
if (shouldChange) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionState.selectImage(image)
|
||||
|
||||
this.close()
|
||||
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
// certain features without printing pages of text to DevTools.
|
||||
image.logo = Boolean(image.logo)
|
||||
image.blockMap = Boolean(image.blockMap)
|
||||
|
||||
analytics.logEvent('Select image', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
}).catch(exceptionReporter.report)
|
||||
}
|
||||
|
||||
selectFile (file) {
|
||||
debug('FileSelector:selectFile', file)
|
||||
|
||||
if (file.isDirectory) {
|
||||
this.navigate(file.path)
|
||||
return
|
||||
}
|
||||
|
||||
if (!supportedFormats.isSupportedImage(file.path)) {
|
||||
const invalidImageError = errors.createUserError({
|
||||
title: 'Invalid image',
|
||||
description: messages.error.invalidImage(file.path)
|
||||
})
|
||||
|
||||
osDialog.showError(invalidImageError)
|
||||
analytics.logEvent('Invalid image', { path: file.path })
|
||||
return
|
||||
}
|
||||
|
||||
debug('FileSelector:getImageMetadata', file)
|
||||
|
||||
const source = new sdk.sourceDestination.File(file.path, sdk.sourceDestination.File.OpenFlags.Read)
|
||||
source.getInnerSource()
|
||||
.then((innerSource) => {
|
||||
return innerSource.getMetadata()
|
||||
.then((imageMetadata) => {
|
||||
debug('FileSelector:getImageMetadata', imageMetadata)
|
||||
imageMetadata.path = file.path
|
||||
imageMetadata.extension = path.extname(file.path).slice(1)
|
||||
return innerSource.getPartitionTable()
|
||||
.then((partitionTable) => {
|
||||
if (partitionTable !== undefined) {
|
||||
imageMetadata.hasMBR = true
|
||||
imageMetadata.partitions = partitionTable.partitions
|
||||
}
|
||||
return this.selectImage(imageMetadata)
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
debug('FileSelector:getImageMetadata', error)
|
||||
const imageError = errors.createUserError({
|
||||
title: 'Error opening image',
|
||||
description: messages.error.openImage(path.basename(file.path), error.message)
|
||||
})
|
||||
|
||||
osDialog.showError(imageError)
|
||||
analytics.logException(error)
|
||||
})
|
||||
}
|
||||
|
||||
onHighlight (file) {
|
||||
this.setState({ highlighted: file })
|
||||
}
|
||||
|
||||
render () {
|
||||
const styles = {
|
||||
display: 'flex',
|
||||
height: 'calc(100vh - 20px)',
|
||||
}
|
||||
return (
|
||||
<rendition.Provider style={ styles }>
|
||||
{/*<RecentFiles flex="0 0 auto"
|
||||
selectFile={ ::this.selectFile }
|
||||
navigate={ ::this.navigate } />*/}
|
||||
<Flex direction="column" grow="1" overflow="auto">
|
||||
<Header flex="0 0 auto" alignItems="baseline">
|
||||
<rendition.Button
|
||||
bg={ colors.secondary.background }
|
||||
color={ colors.primary.color }
|
||||
onClick={ ::this.navigateUp }>
|
||||
<span className="fas fa-angle-left" />
|
||||
Back
|
||||
</rendition.Button>
|
||||
<span className="fas fa-hdd" />
|
||||
<Breadcrumbs
|
||||
path={ this.state.path }
|
||||
navigate={ ::this.navigate }
|
||||
constraintPath={ this.props.constraintpath }
|
||||
constraint={ this.state.constraint }
|
||||
/>
|
||||
</Header>
|
||||
<Main flex="1">
|
||||
<Flex direction="column" grow="1">
|
||||
<FileList path={ this.state.path }
|
||||
constraintPath={ this.props.constraintpath }
|
||||
constraint={ this.state.constraint }
|
||||
onHighlight={ ::this.onHighlight }
|
||||
onSelect={ ::this.selectFile }></FileList>
|
||||
</Flex>
|
||||
</Main>
|
||||
<Footer justifyContent="flex-end">
|
||||
<FilePath file={ this.state.highlighted }></FilePath>
|
||||
<rendition.Button onClick={ ::this.close }>Cancel</rendition.Button>
|
||||
<rendition.Button
|
||||
primary
|
||||
onClick={ ::this.confirmSelection }>
|
||||
Select file
|
||||
</rendition.Button>
|
||||
</Footer>
|
||||
</Flex>
|
||||
</rendition.Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FileSelector.propTypes = {
|
||||
path: propTypes.string,
|
||||
close: propTypes.func,
|
||||
constraintpath: propTypes.string,
|
||||
}
|
||||
|
||||
module.exports = FileSelector
|
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2018 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 path = require('path')
|
||||
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const rendition = require('rendition')
|
||||
|
||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
||||
|
||||
/**
|
||||
* @summary How many directories to show with the breadcrumbs
|
||||
* @type {Number}
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const MAX_DIR_CRUMBS = 3
|
||||
|
||||
/**
|
||||
* @summary Character limit of a filename before a middle-ellipsis is added
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const FILENAME_CHAR_LIMIT_SHORT = 15
|
||||
|
||||
function splitComponents(dirname, root) {
|
||||
const components = []
|
||||
let basename = null
|
||||
root = root || path.parse(dirname).root
|
||||
while( dirname !== root ) {
|
||||
basename = path.basename(dirname)
|
||||
components.unshift({
|
||||
path: dirname,
|
||||
basename: basename,
|
||||
name: basename
|
||||
})
|
||||
dirname = path.join( dirname, '..' )
|
||||
}
|
||||
if (components.length < MAX_DIR_CRUMBS) {
|
||||
components.unshift({
|
||||
path: root,
|
||||
basename: root,
|
||||
name: 'Root'
|
||||
})
|
||||
}
|
||||
return components
|
||||
}
|
||||
|
||||
class Crumb extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<rendition.Button
|
||||
onClick={ ::this.navigate }
|
||||
plain={ true }>
|
||||
<rendition.Txt bold={ this.props.bold }>
|
||||
{ middleEllipsis(this.props.dir.name, FILENAME_CHAR_LIMIT_SHORT) }
|
||||
</rendition.Txt>
|
||||
</rendition.Button>
|
||||
)
|
||||
}
|
||||
|
||||
navigate () {
|
||||
this.props.navigate(this.props.dir.path)
|
||||
}
|
||||
}
|
||||
|
||||
class UnstyledBreadcrumbs extends React.PureComponent {
|
||||
render () {
|
||||
const components = splitComponents(this.props.path).slice(-MAX_DIR_CRUMBS)
|
||||
return (
|
||||
<div className={ this.props.className }>
|
||||
{
|
||||
components.map((dir, index) => {
|
||||
return (
|
||||
<Crumb
|
||||
key={ dir.path }
|
||||
bold={ index === components.length - 1 }
|
||||
dir={ dir }
|
||||
navigate={ ::this.props.navigate }
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Breadcrumbs = styled(UnstyledBreadcrumbs)`
|
||||
font-size: 18px;
|
||||
|
||||
& > button:not(:last-child)::after {
|
||||
content: '/';
|
||||
margin: 9px;
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = Breadcrumbs
|
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2018 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 React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const rendition = require('rendition')
|
||||
const colors = require('./colors')
|
||||
|
||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
||||
|
||||
/**
|
||||
* @summary Flex styled component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
flex: ${ props => props.flex };
|
||||
flex-direction: ${ props => props.direction };
|
||||
justify-content: ${ props => props.justifyContent };
|
||||
align-items: ${ props => props.alignItems };
|
||||
flex-wrap: ${ props => props.wrap };
|
||||
flex-grow: ${ props => props.grow };
|
||||
`
|
||||
|
||||
class RecentFileLink extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render () {
|
||||
const file = this.props.file
|
||||
return (
|
||||
<rendition.Button
|
||||
onClick={ ::this.select }
|
||||
plain={ true }>
|
||||
{ middleEllipsis(file.name, FILENAME_CHAR_LIMIT_SHORT) }
|
||||
</rendition.Button>
|
||||
)
|
||||
}
|
||||
|
||||
select () {
|
||||
this.props.onSelect(this.props.file)
|
||||
}
|
||||
}
|
||||
|
||||
class UnstyledRecentFiles extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
recent: [],
|
||||
favorites: []
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Flex className={ this.props.className }>
|
||||
<h5>Recent</h5>
|
||||
{
|
||||
this.state.recent.map((file) => {
|
||||
<RecentFileLink key={ file.path }
|
||||
file={ file }
|
||||
onSelect={ this.props.selectFile }/>
|
||||
})
|
||||
}
|
||||
<h5>Favorite</h5>
|
||||
{
|
||||
this.state.favorites.map((file) => {
|
||||
<RecentFileLink key={ file.path }
|
||||
file={ file }
|
||||
onSelect={ this.props.navigate }/>
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const RecentFiles = styled(UnstyledRecentFiles)`
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 130px;
|
||||
background-color: ${ colors.secondary.background };
|
||||
padding: 20px;
|
||||
color: ${ colors.secondary.color };
|
||||
|
||||
> h5 {
|
||||
color: ${ colors.secondary.title };
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
> h5:last-of-type {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
> button {
|
||||
margin-bottom: 10px;
|
||||
text-align: start;
|
||||
font-size: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = RecentFiles
|
37
lib/gui/app/components/file-selector/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2018 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 angular = require('angular')
|
||||
const react2angular = require('react2angular').react2angular
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FileSelector'
|
||||
const angularFileSelector = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal')
|
||||
])
|
||||
|
||||
angularFileSelector.component('fileSelector', react2angular(require('./file-selector/file-selector.jsx')))
|
||||
angularFileSelector.controller('FileSelectorController', require('./controllers/file-selector'))
|
||||
angularFileSelector.service('FileSelectorService', require('./services/file-selector'))
|
||||
|
||||
module.exports = MODULE_NAME
|
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2018 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 file selector widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorService.open()
|
||||
*/
|
||||
this.open = () => {
|
||||
modal = ModalService.open({
|
||||
name: 'file-selector',
|
||||
template: require('../templates/file-selector-modal.tpl.html'),
|
||||
controller: 'FileSelectorController as selector',
|
||||
size: 'file-selector-modal'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Close the file selector widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorService.close()
|
||||
*/
|
||||
this.close = () => {
|
||||
if (modal) {
|
||||
modal.close()
|
||||
}
|
||||
modal = null
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,9 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.modal-update-notifier .checkbox {
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
font-size: 11px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.modal-file-selector-modal {
|
||||
width: calc(100vw - 10px);
|
||||
|
||||
> .modal-content {
|
||||
height: calc(100vh - 20px);
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<file-selector
|
||||
constraintpath="selector.getFolderConstraint()"
|
||||
path="selector.getPath()"
|
||||
close="selector.close"></file-selector>
|
49
lib/gui/app/components/flash-another/flash-another.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2018 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-next-line no-unused-vars
|
||||
const React = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const { position, right } = require('styled-system')
|
||||
const { BaseButton, ThemedProvider } = require('../../styled-components')
|
||||
|
||||
const Div = styled.div `
|
||||
${position}
|
||||
${right}
|
||||
`
|
||||
|
||||
const FlashAnother = (props) => {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<Div position='absolute' right='152px'>
|
||||
<BaseButton
|
||||
primary
|
||||
onClick={props.onClick.bind(null, { preserveImage: true })}>
|
||||
Flash Another
|
||||
</BaseButton>
|
||||
</Div>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
FlashAnother.propTypes = {
|
||||
onClick: PropTypes.func
|
||||
}
|
||||
|
||||
module.exports = FlashAnother
|
34
lib/gui/app/components/flash-another/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2018 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.FlashAnother
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FlashAnother'
|
||||
const FlashAnother = angular.module(MODULE_NAME, [])
|
||||
|
||||
FlashAnother.component(
|
||||
'flashAnother',
|
||||
react2angular(require('./flash-another.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
@@ -14,21 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.FlashErrorModal
|
||||
*/
|
||||
|
||||
const angular = require('angular');
|
||||
const MODULE_NAME = 'Etcher.Components.FlashErrorModal';
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.FlashErrorModal'
|
||||
const FlashErrorModal = angular.module(MODULE_NAME, [
|
||||
require('../warning-modal/warning-modal'),
|
||||
require('../../models/flash-state'),
|
||||
require('../../models/selection-state'),
|
||||
require('../../modules/analytics')
|
||||
]);
|
||||
require('../warning-modal/warning-modal')
|
||||
])
|
||||
|
||||
FlashErrorModal.service('FlashErrorModalService', require('./services/flash-error-modal'));
|
||||
FlashErrorModal.service('FlashErrorModalService', require('./services/flash-error-modal'))
|
||||
|
||||
module.exports = MODULE_NAME;
|
||||
module.exports = MODULE_NAME
|
@@ -14,10 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
module.exports = function(WarningModalService, FlashStateModel, SelectionStateModel, AnalyticsService) {
|
||||
const flashState = require('../../../models/flash-state')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const store = require('../../../models/store')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
|
||||
module.exports = function (WarningModalService) {
|
||||
/**
|
||||
* @summary Open the flash error modal
|
||||
* @function
|
||||
@@ -34,14 +38,16 @@ module.exports = function(WarningModalService, FlashStateModel, SelectionStateMo
|
||||
confirmationLabel: 'Retry',
|
||||
description: message
|
||||
}).then((confirmed) => {
|
||||
FlashStateModel.resetState();
|
||||
flashState.resetState()
|
||||
|
||||
if (confirmed) {
|
||||
AnalyticsService.logEvent('Restart after failure');
|
||||
analytics.logEvent('Restart after failure', {
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
} else {
|
||||
SelectionStateModel.clear();
|
||||
selectionState.clear()
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
66
lib/gui/app/components/flash-results/flash-results.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2018 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 React = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const _ = require('lodash')
|
||||
const styled = require('styled-components').default
|
||||
const { position, left, top, space } = require('styled-system')
|
||||
const { Underline } = require('./../../styled-components')
|
||||
|
||||
const Div = styled.div `
|
||||
${position}
|
||||
${top}
|
||||
${left}
|
||||
${space}
|
||||
`
|
||||
|
||||
/* eslint-disable no-inline-comments */
|
||||
|
||||
const FlashResults = (props) => {
|
||||
return (
|
||||
<Div position='absolute' left='153px' top='66px'>
|
||||
<div className="inline-flex title">
|
||||
<span className="tick tick--success space-right-medium"></span>
|
||||
<h3>Flash Complete!</h3>
|
||||
</div>
|
||||
<Div className="results" mt='11px' mr='0' mb='0' ml='40px'>
|
||||
<Underline
|
||||
tooltip={props.errors()}>
|
||||
{_.map(props.results.devices, (quantity, type) => {
|
||||
return (quantity) ? (
|
||||
<div key={type} className={`target-status-line target-status-${type}`}>
|
||||
<span className="target-status-dot"></span>
|
||||
<span className="target-status-quantity">{ quantity }</span>
|
||||
<span className="target-status-message">{ props.message[type](quantity) }</span>
|
||||
</div>
|
||||
) : null
|
||||
})}
|
||||
</Underline>
|
||||
</Div>
|
||||
</Div>
|
||||
)
|
||||
}
|
||||
|
||||
FlashResults.propTypes = {
|
||||
results: PropTypes.object,
|
||||
message: PropTypes.object,
|
||||
errors: PropTypes.func
|
||||
}
|
||||
|
||||
module.exports = FlashResults
|
34
lib/gui/app/components/flash-results/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2018 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.FlashResults
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FlashResults'
|
||||
const FlashResults = angular.module(MODULE_NAME, [])
|
||||
|
||||
FlashResults.component(
|
||||
'flashResults',
|
||||
react2angular(require('./flash-results.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
128
lib/gui/app/components/image-selector/image-selector.jsx
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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 no-unused-vars */
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const { Badge, DropDownButton, Select } = require('rendition')
|
||||
const { default: styled } = require('styled-components')
|
||||
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
|
||||
const shared = require('./../../../../gui/app/modules/units')
|
||||
const {
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
StepSelection,
|
||||
Footer,
|
||||
Underline,
|
||||
DetailsText,
|
||||
ChangeButton,
|
||||
ThemedProvider
|
||||
} = require('./../../styled-components')
|
||||
|
||||
const DropdownItem = styled.p`
|
||||
padding-top: 10px;
|
||||
text-align: left;
|
||||
width: 150px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const DropdownItemIcon = styled.i`
|
||||
padding-right: 10px;
|
||||
`
|
||||
|
||||
const SelectImageButton = (props) => {
|
||||
if (props.hasImage) {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={props.showSelectedImageDetails}
|
||||
tooltip={props.imageBasename}
|
||||
>
|
||||
{/* eslint-disable no-magic-numbers */}
|
||||
{ middleEllipsis(props.imageName || props.imageBasename, 20) }
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={props.deselectImage}
|
||||
>
|
||||
Remove
|
||||
</ChangeButton>
|
||||
}
|
||||
<DetailsText>
|
||||
{shared.bytesToClosestUnit(props.imageSize)}
|
||||
</DetailsText>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepSelection>
|
||||
<DropDownButton
|
||||
primary
|
||||
label={
|
||||
<div onClick={props.openImageSelector}>Select image</div>
|
||||
}
|
||||
style={{height: '48px'}}
|
||||
>
|
||||
<DropdownItem
|
||||
onClick={props.openImageSelector}
|
||||
>
|
||||
<DropdownItemIcon className="far fa-file"/>
|
||||
Select image file
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
<DropdownItemIcon className="far fa-copy"/>
|
||||
Duplicate drive
|
||||
</DropdownItem>
|
||||
</DropDownButton>
|
||||
<Footer>
|
||||
{ props.mainSupportedExtensions.join(', ') }, and{' '}
|
||||
<Underline
|
||||
tooltip={ props.extraSupportedExtensions.join(', ') }
|
||||
>
|
||||
many more
|
||||
</Underline>
|
||||
</Footer>
|
||||
</StepSelection>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
SelectImageButton.propTypes = {
|
||||
openImageSelector: propTypes.func,
|
||||
openDriveSelector: propTypes.func,
|
||||
mainSupportedExtensions: propTypes.array,
|
||||
extraSupportedExtensions: propTypes.array,
|
||||
hasImage: propTypes.bool,
|
||||
showSelectedImageDetails: propTypes.func,
|
||||
imageName: propTypes.string,
|
||||
imageBasename: propTypes.string,
|
||||
deselectImage: propTypes.func,
|
||||
flashing: propTypes.bool,
|
||||
imageSize: propTypes.number,
|
||||
sourceType: propTypes.string
|
||||
}
|
||||
|
||||
module.exports = SelectImageButton
|
34
lib/gui/app/components/image-selector/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2018 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.ImageSelector
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.ImageSelector'
|
||||
const SelectImageButton = angular.module(MODULE_NAME, [])
|
||||
|
||||
SelectImageButton.component(
|
||||
'imageSelector',
|
||||
react2angular(require('./image-selector.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
@@ -14,18 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.Modal
|
||||
*/
|
||||
|
||||
const angular = require('angular');
|
||||
const MODULE_NAME = 'Etcher.Components.Modal';
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.Modal'
|
||||
const Modal = angular.module(MODULE_NAME, [
|
||||
require('angular-ui-bootstrap')
|
||||
]);
|
||||
])
|
||||
|
||||
Modal.service('ModalService', require('./services/modal'));
|
||||
Modal.service('ModalService', require('./services/modal'))
|
||||
|
||||
module.exports = MODULE_NAME;
|
||||
module.exports = MODULE_NAME
|
100
lib/gui/app/components/modal/services/modal.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 store = require('../../../models/store')
|
||||
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,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
const modal = $uibModal.open({
|
||||
animation: true,
|
||||
template: options.template,
|
||||
controller: options.controller,
|
||||
size: options.size,
|
||||
resolve: options.resolve,
|
||||
backdrop: 'static'
|
||||
})
|
||||
|
||||
return {
|
||||
close: modal.close,
|
||||
result: $q((resolve, reject) => {
|
||||
modal.result.then((value) => {
|
||||
analytics.logEvent('Modal accepted', {
|
||||
name: options.name,
|
||||
value,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
resolve(value)
|
||||
}).catch((error) => {
|
||||
// Bootstrap doesn't 'resolve' these but cancels the dialog
|
||||
if (error === 'escape key press') {
|
||||
analytics.logEvent('Modal rejected', {
|
||||
name: options.name,
|
||||
method: error,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
return resolve()
|
||||
}
|
||||
|
||||
analytics.logEvent('Modal rejected', {
|
||||
name: options.name,
|
||||
value: error,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
return reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,6 +20,7 @@
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@@ -69,7 +70,7 @@
|
||||
// window, causing the window content to overflow and get
|
||||
// pushed to the bottom.
|
||||
// The `!important` flag is needed since UI Bootstrap inlines
|
||||
// the styles programatically to the element.
|
||||
// the styles programmatically to the element.
|
||||
.modal-open {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
@@ -82,6 +83,7 @@
|
||||
.modal-footer {
|
||||
flex-grow: 0;
|
||||
border: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
@@ -101,5 +103,4 @@
|
||||
.modal-dialog {
|
||||
margin: 0;
|
||||
position: initial;
|
||||
max-width: 50%;
|
||||
}
|
@@ -14,15 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.ProgressButton
|
||||
*/
|
||||
|
||||
const angular = require('angular');
|
||||
const MODULE_NAME = 'Etcher.Components.ProgressButton';
|
||||
const ProgressButton = angular.module(MODULE_NAME, []);
|
||||
ProgressButton.directive('progressButton', require('./directives/progress-button'));
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
module.exports = MODULE_NAME;
|
||||
const MODULE_NAME = 'Etcher.Components.ProgressButton'
|
||||
const ProgressButton = angular.module(MODULE_NAME, [])
|
||||
|
||||
ProgressButton.component(
|
||||
'progressButton',
|
||||
react2angular(require('./progress-button.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
161
lib/gui/app/components/progress-button/progress-button.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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 React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const Color = require('color')
|
||||
|
||||
const {
|
||||
default: styled,
|
||||
css,
|
||||
keyframes
|
||||
} = require('styled-components')
|
||||
|
||||
const { ProgressBar, Provider } = require('rendition')
|
||||
|
||||
const { colors } = require('./../../theme')
|
||||
const { StepButton, StepSelection } = require('./../../styled-components')
|
||||
|
||||
const darkenForegroundStripes = 0.18
|
||||
const desaturateForegroundStripes = 0.2
|
||||
const progressButtonStripesForegroundColor = Color(colors.primary.background)
|
||||
.darken(darkenForegroundStripes)
|
||||
.desaturate(desaturateForegroundStripes)
|
||||
.string()
|
||||
|
||||
const desaturateBackgroundStripes = 0.05
|
||||
const progressButtonStripesBackgroundColor = Color(colors.primary.background)
|
||||
.desaturate(desaturateBackgroundStripes)
|
||||
.string()
|
||||
|
||||
const ProgressButtonStripes = keyframes `
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 20px 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const ProgressButtonStripesRule = css `
|
||||
${ProgressButtonStripes} 1s linear infinite;
|
||||
`
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar) `
|
||||
> div {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
line-height: 48px;
|
||||
|
||||
background: ${Color(colors.warning.background).darken(darkenForegroundStripes).string()};
|
||||
`
|
||||
|
||||
const FlashProgressBarValidating = styled(FlashProgressBar) `
|
||||
|
||||
// Notice that we add 0.01 to certain gradient stop positions.
|
||||
// That workarounds a Chrome rendering issue where diagonal
|
||||
// lines look spiky.
|
||||
// See https://github.com/resin-io/etcher/issues/472
|
||||
|
||||
background-image: -webkit-gradient(linear, 0 0, 100% 100%,
|
||||
color-stop(0.25, ${progressButtonStripesForegroundColor}),
|
||||
color-stop(0.26, ${progressButtonStripesBackgroundColor}),
|
||||
color-stop(0.50, ${progressButtonStripesBackgroundColor}),
|
||||
color-stop(0.51, ${progressButtonStripesForegroundColor}),
|
||||
color-stop(0.75, ${progressButtonStripesForegroundColor}),
|
||||
color-stop(0.76 , ${progressButtonStripesBackgroundColor}),
|
||||
to(${progressButtonStripesBackgroundColor}));
|
||||
|
||||
background-color: white;
|
||||
|
||||
animation: ${ProgressButtonStripesRule};
|
||||
overflow: hidden;
|
||||
|
||||
background-size: 20px 20px;
|
||||
`
|
||||
|
||||
/**
|
||||
* Progress Button component
|
||||
*/
|
||||
class ProgressButton extends React.Component {
|
||||
render () {
|
||||
if (this.props.active) {
|
||||
if (this.props.striped) {
|
||||
return (
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<FlashProgressBarValidating
|
||||
primary
|
||||
emphasized
|
||||
value= { this.props.percentage }
|
||||
>
|
||||
{ this.props.label }
|
||||
</FlashProgressBarValidating>
|
||||
</StepSelection>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<FlashProgressBar
|
||||
warning
|
||||
emphasized
|
||||
value= { this.props.percentage }
|
||||
>
|
||||
{ this.props.label }
|
||||
</FlashProgressBar>
|
||||
</StepSelection>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<StepButton
|
||||
onClick= { this.props.callback }
|
||||
disabled= { this.props.disabled }
|
||||
>
|
||||
{this.props.label}
|
||||
</StepButton>
|
||||
</StepSelection>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProgressButton.propTypes = {
|
||||
striped: propTypes.bool,
|
||||
active: propTypes.bool,
|
||||
percentage: propTypes.number,
|
||||
label: propTypes.string,
|
||||
disabled: propTypes.bool,
|
||||
callback: propTypes.func
|
||||
}
|
||||
|
||||
module.exports = ProgressButton
|