Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c06a33018 | ||
|
|
af4155b2df | ||
|
|
2a67e7e6cf | ||
|
|
351c5caf23 | ||
|
|
36780a95e7 | ||
|
|
9065a8678a | ||
|
|
016d3ee78a | ||
|
|
4b40420bba | ||
|
|
0e872b0dd0 | ||
|
|
7638111598 | ||
|
|
a0e59ad288 | ||
|
|
3c6103dc50 | ||
|
|
b98472f973 | ||
|
|
3602eacc88 | ||
|
|
8b84c040c7 | ||
|
|
6aa19eada6 | ||
|
|
8e92183de2 | ||
|
|
1a951f3f44 | ||
|
|
66ead93f05 | ||
|
|
d22899b4a5 | ||
|
|
77d4bc6fc5 | ||
|
|
961d42d74f | ||
|
|
c58ade2132 | ||
|
|
592c220f18 | ||
|
|
4da0e82e9b | ||
|
|
f766bef10c | ||
|
|
30ba9fb023 | ||
|
|
f3e847da85 | ||
|
|
0f854441e4 | ||
|
|
f31a4e38bb | ||
|
|
26bb3e4944 | ||
|
|
fb1fb7869c | ||
|
|
a1e67400bf | ||
|
|
a26aa1e70c | ||
|
|
78395412f4 | ||
|
|
1f72db3cfc | ||
|
|
c333a1e132 | ||
|
|
a0a989d7a7 | ||
|
|
5b07b8a706 | ||
|
|
98a358b756 | ||
|
|
548b68e208 | ||
|
|
683def4215 | ||
|
|
5596c12f19 | ||
|
|
db9c1d5105 | ||
|
|
b52e70ec61 | ||
|
|
305bb9afbe | ||
|
|
d121954018 | ||
|
|
e51b1648cb | ||
|
|
ccc72ee70a | ||
|
|
48c62f4e02 | ||
|
|
9b55b2ef26 | ||
|
|
5b91177348 | ||
|
|
f8c43a07f7 | ||
|
|
5eba9cdbe2 | ||
|
|
8ee42c290e | ||
|
|
c2b3954dfa | ||
|
|
e61599e9c5 | ||
|
|
1fed772a27 | ||
|
|
de85d0f324 | ||
|
|
c89671d99d | ||
|
|
155b901ade | ||
|
|
8e098deab8 | ||
|
|
2b5ea7d467 | ||
|
|
91efa5a009 | ||
|
|
4ec5bec719 | ||
|
|
84ac41f147 | ||
|
|
e9a2985291 | ||
|
|
9b28919203 | ||
|
|
8d43836787 | ||
|
|
48d0dadf5d | ||
|
|
2f9ef21251 | ||
|
|
8e4238275c | ||
|
|
32d5ea99aa | ||
|
|
da2e17cd37 | ||
|
|
539f5bce97 | ||
|
|
228552c8a0 | ||
|
|
ed8d76a54a | ||
|
|
2f4d47a0ab | ||
|
|
9b443de137 | ||
|
|
efb5bf0e57 | ||
|
|
c420747bb9 | ||
|
|
33223e7b37 | ||
|
|
86cc0c62a1 | ||
|
|
96bd2e36dd | ||
|
|
9db527773b | ||
|
|
9da1acafd9 | ||
|
|
5a05d34776 | ||
|
|
6b9d2b66e2 | ||
|
|
d4096a6de8 | ||
|
|
3709d122a6 | ||
|
|
0368cda57a | ||
|
|
433cb6acd5 | ||
|
|
56dceaefb0 | ||
|
|
80da23c4e4 | ||
|
|
353968ef8e | ||
|
|
64ece1b364 | ||
|
|
13383f7345 | ||
|
|
cae7222636 | ||
|
|
2aabe80f2b | ||
|
|
d0683f1392 | ||
|
|
389352cefa | ||
|
|
d04b654c3e | ||
|
|
1369c9b35c | ||
|
|
9957090d25 | ||
|
|
8a002fb0a8 | ||
|
|
cae8dde4c8 | ||
|
|
cb176534f9 | ||
|
|
f7fa9a8ffc | ||
|
|
347384a547 | ||
|
|
e272efb3b9 | ||
|
|
5255bcb842 | ||
|
|
d3cd53f3e0 | ||
|
|
6d28c4c81b | ||
|
|
a45354e1af | ||
|
|
28245d36ba | ||
|
|
582ef280b5 | ||
|
|
56b7f2ffb7 | ||
|
|
cb3ba4f0a7 | ||
|
|
e238b10f1e | ||
|
|
bbc22d9ca8 | ||
|
|
7b3c960c1f | ||
|
|
4dbc74fccb | ||
|
|
f71b1bc8d4 | ||
|
|
00c2e1f9a6 | ||
|
|
50f69613dd | ||
|
|
4a23b53b1b | ||
|
|
dcd21935a9 | ||
|
|
ff37d5cfb9 | ||
|
|
bb8053d86d | ||
|
|
5efaa54255 | ||
|
|
b38507230d | ||
|
|
7451083c70 | ||
|
|
be46780674 | ||
|
|
99b5d067df | ||
|
|
f34eb225ea | ||
|
|
18cc36a837 | ||
|
|
da403e3cbd | ||
|
|
b4431cf426 | ||
|
|
e714269b9b | ||
|
|
b9518a63f8 | ||
|
|
0627801bb0 | ||
|
|
3adeece64d | ||
|
|
aed45078c5 | ||
|
|
55bfb0241b | ||
|
|
7e90679706 | ||
|
|
4897bdbf84 | ||
|
|
7a02f8cacd | ||
|
|
a65d594fbc | ||
|
|
f32fce2f80 | ||
|
|
26e054702a | ||
|
|
16f27132cb | ||
|
|
7613176f69 | ||
|
|
470bea4fca | ||
|
|
363febdb73 | ||
|
|
062ca4e205 | ||
|
|
49053342a5 | ||
|
|
fd582c67a8 | ||
|
|
706b18808b | ||
|
|
a6ae8d5376 | ||
|
|
5034f8c853 | ||
|
|
e916247013 | ||
|
|
c052686de4 | ||
|
|
31aacef131 | ||
|
|
932369b705 | ||
|
|
b835c34786 | ||
|
|
c9e1ba815a | ||
|
|
374a85e2f6 | ||
|
|
4282d0b93d | ||
|
|
549862b3bb | ||
|
|
4691c1b494 | ||
|
|
8125abc657 | ||
|
|
8cba7739f5 | ||
|
|
0b39fe40b1 | ||
|
|
cae078ffe6 | ||
|
|
bfb931435f | ||
|
|
c878f72f2e | ||
|
|
32494c057a | ||
|
|
e732685fff | ||
|
|
d9d5e0cbc9 | ||
|
|
0e1d7b541e | ||
|
|
8a602d94b7 | ||
|
|
155889dbcc | ||
|
|
56ac6662b6 | ||
|
|
adb196549b | ||
|
|
999facf062 | ||
|
|
fa0115f2db | ||
|
|
c0a4c0e48d | ||
|
|
3a5d2205fe | ||
|
|
1083b8f859 | ||
|
|
252d4c1de6 | ||
|
|
0c44d2c9e4 | ||
|
|
5eebc06514 | ||
|
|
eac1423177 | ||
|
|
ae88780281 | ||
|
|
4bf9d48d70 | ||
|
|
df6d4b5a9a | ||
|
|
68202e2b23 | ||
|
|
acd5b35ca1 | ||
|
|
fae1ca3138 | ||
|
|
bd220d2690 | ||
|
|
d31f1016b8 | ||
|
|
3b52be0f79 | ||
|
|
0e11c56f7f | ||
|
|
8019981d1a | ||
|
|
403a58e216 | ||
|
|
3341d0ccaa | ||
|
|
d42e83100a | ||
|
|
50e9883d82 | ||
|
|
f5aedac6a3 | ||
|
|
a3f4aa8ed3 | ||
|
|
9c839aaad8 | ||
|
|
3a7ca8ccd5 | ||
|
|
fef42293bc | ||
|
|
1f9db42c80 | ||
|
|
8e5657ef2d | ||
|
|
a94105dd27 | ||
|
|
cf214dc0d0 | ||
|
|
6dfdf00437 | ||
|
|
b3743a4c73 | ||
|
|
3c0d6c2293 | ||
|
|
89b589fd2e | ||
|
|
42c577988d | ||
|
|
d0e47b6b4d | ||
|
|
f44a127012 | ||
|
|
fa2f5454ce | ||
|
|
1021bb8b2b | ||
|
|
cf4ad36ca2 | ||
|
|
4fe9c627b5 | ||
|
|
262e636e8d | ||
|
|
bbe79ed6e2 | ||
|
|
bc96a5013a | ||
|
|
66ba7b74d5 | ||
|
|
38e5aa98e3 | ||
|
|
a91504b173 | ||
|
|
4685e88c86 | ||
|
|
e0bb82d919 | ||
|
|
7035be3286 | ||
|
|
c961cfadb8 | ||
|
|
7267a2ee88 | ||
|
|
bc03382dc5 | ||
|
|
891c33ba68 | ||
|
|
e7260c74a4 | ||
|
|
ed694a7044 | ||
|
|
14fc77c08c | ||
|
|
f3079648cf | ||
|
|
7272ec7899 | ||
|
|
ac9ac701f7 | ||
|
|
7fce572823 | ||
|
|
6186a80dad | ||
|
|
ade6576285 | ||
|
|
61c58277f3 | ||
|
|
c361d34a6d | ||
|
|
5887a6615a | ||
|
|
7ba4cf5be7 | ||
|
|
0daf872d2b | ||
|
|
75ec8ee9b1 | ||
|
|
183c152e40 | ||
|
|
d6a807ba41 | ||
|
|
24e0abf667 | ||
|
|
eb8261e240 | ||
|
|
84cc07da90 | ||
|
|
b0f3837b47 | ||
|
|
e29664ca45 | ||
|
|
ef36ee39f6 | ||
|
|
73dadb815e | ||
|
|
4945501703 | ||
|
|
77c87fbdeb | ||
|
|
2ef5b42f4a | ||
|
|
9c4bc2ba52 | ||
|
|
80da4e73e3 | ||
|
|
eadd58d149 | ||
|
|
1fc82f8f6d | ||
|
|
9bd7043b2c | ||
|
|
aaeef6fd06 | ||
|
|
1195902bd5 | ||
|
|
c022d0f33d | ||
|
|
996ae775f3 | ||
|
|
ed44dda8fb | ||
|
|
effba13e73 | ||
|
|
65886c3b52 | ||
|
|
8bd4dd366a | ||
|
|
4e19578e34 | ||
|
|
c7fd0bc95e | ||
|
|
29e2f03b1a | ||
|
|
4cd6f330da | ||
|
|
79521eac7c | ||
|
|
2736499d68 | ||
|
|
1cbb781e8d | ||
|
|
e849a49f6f | ||
|
|
0114d2c1eb | ||
|
|
365537012f | ||
|
|
3e435cb64d | ||
|
|
6981382793 | ||
|
|
f293ba84d1 | ||
|
|
55cf507a96 | ||
|
|
e71c2439cd | ||
|
|
9bd253951c | ||
|
|
eda08c9aea | ||
|
|
9414f66893 | ||
|
|
f4725b5dd8 | ||
|
|
fd8be47494 | ||
|
|
2cebe2ca7c | ||
|
|
74135bab67 | ||
|
|
a856340d01 | ||
|
|
bd8939c358 | ||
|
|
9d3f3d61eb | ||
|
|
bdeaffcb7d | ||
|
|
344e1df375 | ||
|
|
d5b52dcae7 | ||
|
|
0faeb24761 | ||
|
|
284b579d0c | ||
|
|
5a1c24cdda | ||
|
|
024d48a273 | ||
|
|
32155d652d | ||
|
|
874a8bb0f0 | ||
|
|
73470640c7 | ||
|
|
679f731f4c | ||
|
|
a41ad11f8c | ||
|
|
7cc6d38dc2 | ||
|
|
1e6ee7ab37 | ||
|
|
c50c31ad80 | ||
|
|
5f8557fb13 | ||
|
|
47b20fa3e4 | ||
|
|
b5c8ed60a9 | ||
|
|
5e3d382b85 | ||
|
|
fed0107dd1 | ||
|
|
ca9b9c633a | ||
|
|
fb4c6b3f33 | ||
|
|
aa51c0cc5c | ||
|
|
e5236c5500 | ||
|
|
321f7a8bbc | ||
|
|
1ff7a3fe16 | ||
|
|
a1c74c018d | ||
|
|
5df39ea51a | ||
|
|
8a9591be9f | ||
|
|
3cc8fd41f6 | ||
|
|
25df3dd989 | ||
|
|
abe7bdab23 | ||
|
|
16bea01ba5 | ||
|
|
7417a82931 | ||
|
|
4c41db8595 | ||
|
|
5da87ffca3 | ||
|
|
919e9cb6b6 | ||
|
|
14279283d6 | ||
|
|
77a98112a1 | ||
|
|
08fb3f52bf | ||
|
|
b37ba895e5 | ||
|
|
33a236edd1 | ||
|
|
f7930fb523 | ||
|
|
88cad66076 | ||
|
|
f0afedf896 | ||
|
|
38234dd0a3 | ||
|
|
83e834bea5 | ||
|
|
38be3cca14 | ||
|
|
4908cc6110 | ||
|
|
ab4a9015d1 | ||
|
|
73ab813efc | ||
|
|
f3570c5069 | ||
|
|
4c564eaa63 | ||
|
|
a289842e32 | ||
|
|
9d697e7a3f | ||
|
|
e96e5345e6 | ||
|
|
7c97bc2ef6 | ||
|
|
d55c5c067d | ||
|
|
083ac3fbc2 | ||
|
|
5c8fe3d6e0 | ||
|
|
f034c5240c | ||
|
|
6421187db4 | ||
|
|
6fb385a95f | ||
|
|
c47fea09ed | ||
|
|
ef17ccd9cb | ||
|
|
3f173aa4d4 | ||
|
|
faf15081e0 | ||
|
|
4306f53c70 | ||
|
|
52a8aac6c7 | ||
|
|
7660192dcb | ||
|
|
53ceeb5600 | ||
|
|
5cd7a57f08 | ||
|
|
e96a3da09b | ||
|
|
90c198d487 | ||
|
|
5b56333516 | ||
|
|
c87f65ca76 | ||
|
|
0f27c8aa7a | ||
|
|
b5880aef46 | ||
|
|
fb46eaf0e4 | ||
|
|
c1040097e3 | ||
|
|
333758b73d | ||
|
|
3c2a4e1b76 | ||
|
|
f3d42c107f | ||
|
|
2ece4dc728 | ||
|
|
dd2975bd0e | ||
|
|
fdf03dff9b | ||
|
|
48fa762f7f | ||
|
|
7cd9a86920 | ||
|
|
db2caaad59 | ||
|
|
3b2cb93d8b | ||
|
|
9822753ffd | ||
|
|
8712907182 | ||
|
|
40055ee069 | ||
|
|
a33f175867 | ||
|
|
107ba63a5a | ||
|
|
b66ad7e247 | ||
|
|
7eb15b6666 | ||
|
|
2ed29f68dd | ||
|
|
d89d595b57 | ||
|
|
2b34ef5e67 | ||
|
|
d6f7146447 | ||
|
|
006ba5f0a5 | ||
|
|
5df390b0fd | ||
|
|
3eb25fed91 | ||
|
|
9b06f792ea | ||
|
|
c116152605 | ||
|
|
b9f8d55809 | ||
|
|
89e697fcd6 | ||
|
|
9df5314c08 | ||
|
|
d01ba1cdff | ||
|
|
ed5ccf133f | ||
|
|
e1decd4819 | ||
|
|
3c9f477b89 | ||
|
|
5cfbf2cab5 | ||
|
|
89272fbaa9 | ||
|
|
5d572f451c | ||
|
|
319136ecc8 | ||
|
|
3a4aa06331 | ||
|
|
e283a118f6 | ||
|
|
e67d1b0179 | ||
|
|
9805d98dd8 | ||
|
|
009ad25983 | ||
|
|
9a65124289 | ||
|
|
4d7c81ee69 | ||
|
|
ed91cb5ba1 | ||
|
|
8b04f67f71 | ||
|
|
2e183f0538 | ||
|
|
6b022bbea7 | ||
|
|
6bdb7d71ec | ||
|
|
0ebe6ab9b3 | ||
|
|
5e73deeb4f | ||
|
|
02d81aa407 | ||
|
|
46deeb857e | ||
|
|
ca55d2e620 | ||
|
|
88974d277f | ||
|
|
1239afafeb | ||
|
|
415877c067 | ||
|
|
0a0779ffa1 | ||
|
|
9cf34ea983 | ||
|
|
83a7d23b38 | ||
|
|
7d19ad91a6 | ||
|
|
db13699790 | ||
|
|
4a8fc24ff6 | ||
|
|
3ed41626b1 | ||
|
|
b3e25cfe79 | ||
|
|
00bb15a891 | ||
|
|
94da51128a | ||
|
|
b573f09494 | ||
|
|
f05e36de34 | ||
|
|
44c7c70793 | ||
|
|
bcbca7aceb | ||
|
|
9515458545 | ||
|
|
219e90cad8 | ||
|
|
ba23c8019d | ||
|
|
2fef96a2b2 | ||
|
|
2d8cb9748d | ||
|
|
eaefbb7ad0 | ||
|
|
d5f28253f8 | ||
|
|
ac451d6b4a | ||
|
|
b5e0e63766 | ||
|
|
49538ea55a | ||
|
|
23ac9b8e02 | ||
|
|
4bf2bee7c8 | ||
|
|
e742d8426c | ||
|
|
7a223fd3dc | ||
|
|
6bdc995eb6 | ||
|
|
72cbcd46d1 | ||
|
|
f1e51c71a3 | ||
|
|
95e88c0eee | ||
|
|
a54037ef0e | ||
|
|
83bb6fc73e | ||
|
|
f8f1f0ec08 | ||
|
|
f7a19b92a8 | ||
|
|
dc76b78337 | ||
|
|
f6f3e94a97 | ||
|
|
89cede25b1 | ||
|
|
8e8eec3cc6 | ||
|
|
a0d8ca4019 | ||
|
|
1f0581afbc | ||
|
|
70d4902b87 | ||
|
|
a93a99e3f0 | ||
|
|
4c209159e6 | ||
|
|
b09a248c96 | ||
|
|
6f09ed9c2a | ||
|
|
c1ae0ecd86 | ||
|
|
1e9a81fcf4 | ||
|
|
2061b26ed6 | ||
|
|
88ce4a76e9 | ||
|
|
9b4ef4c0d2 | ||
|
|
dca2cd67e5 | ||
|
|
6bab353088 | ||
|
|
e6cc44ca37 | ||
|
|
d039c5dbf5 | ||
|
|
c155fff8b0 | ||
|
|
11202a8d6b | ||
|
|
475127f5b1 | ||
|
|
feee5be2f8 | ||
|
|
678c7f1578 | ||
|
|
d2fcb8071b | ||
|
|
0ab35dc867 | ||
|
|
377d221191 | ||
|
|
cbdab9d662 | ||
|
|
6875ab392e | ||
|
|
58ec0f015c | ||
|
|
2aef0e776e | ||
|
|
b2a7d6b2a4 | ||
|
|
303d6bc79a | ||
|
|
d028e7c915 | ||
|
|
f04631b6c1 | ||
|
|
c1bb524b59 | ||
|
|
4a38fda293 | ||
|
|
b98ad2b572 | ||
|
|
6118b94b09 | ||
|
|
d0c1ee1a82 | ||
|
|
525715a911 | ||
|
|
afbc0c9e6c | ||
|
|
c1a9cdf88b | ||
|
|
d281e61b8a | ||
|
|
2fbbc10497 | ||
|
|
e8226ca97d | ||
|
|
08f2dbe937 | ||
|
|
68813edb51 | ||
|
|
a32261414b | ||
|
|
42f67e7b21 | ||
|
|
7d9b0ed79d | ||
|
|
47f1b5c4c3 | ||
|
|
432322694d | ||
|
|
889e76cbae | ||
|
|
4a689e5c06 | ||
|
|
3113f324cc | ||
|
|
64addb1fb0 | ||
|
|
e6facf60c1 | ||
|
|
00cb5cf6c6 | ||
|
|
3e8dbebe42 | ||
|
|
dcdef4aa4e | ||
|
|
590389abdc | ||
|
|
18a8f0c142 | ||
|
|
a8e8ab6f04 | ||
|
|
45ed35467a | ||
|
|
eb49cb296a | ||
|
|
c2344d50d2 | ||
|
|
7786e113d9 | ||
|
|
f8b51870d8 | ||
|
|
601c28dcf8 | ||
|
|
49379af856 | ||
|
|
8347f79756 | ||
|
|
c102128d5a | ||
|
|
fcff053136 | ||
|
|
c3641cfa04 | ||
|
|
6eba3cbdcc | ||
|
|
6393c59d5c | ||
|
|
543e86bc91 | ||
|
|
ea5fdd426c | ||
|
|
fecc499cc5 | ||
|
|
8d0a1524b9 | ||
|
|
918e91965a | ||
|
|
15bab9ff86 | ||
|
|
7d879b59cf | ||
|
|
72f7e058d6 | ||
|
|
a65e044b5c | ||
|
|
a4c4590816 | ||
|
|
6fbb816be3 | ||
|
|
ae0ba026f2 | ||
|
|
cc2a44d10a | ||
|
|
3e6eb16974 | ||
|
|
4212284fc4 | ||
|
|
8b35ff8f95 | ||
|
|
4d00ee9fd4 | ||
|
|
590db5304d | ||
|
|
e519ce2f13 | ||
|
|
8221920fab | ||
|
|
5de7ed7d9d | ||
|
|
a1586e864a | ||
|
|
60745ff8bb | ||
|
|
4ac20aa5c2 | ||
|
|
48faec9741 | ||
|
|
c0f4e95a67 | ||
|
|
d5c8c8bd96 | ||
|
|
80ff396879 | ||
|
|
bf42abc88d | ||
|
|
eaa064ea8d | ||
|
|
e804436669 | ||
|
|
08d07b34a4 | ||
|
|
74ba7db13c | ||
|
|
0fced3aefd | ||
|
|
13c9217f9f | ||
|
|
346742b242 | ||
|
|
aefe273516 | ||
|
|
337d19c33a | ||
|
|
ebc6f91dea | ||
|
|
25446aa9e4 | ||
|
|
f2859c053b | ||
|
|
49cd192817 | ||
|
|
9bdd09c6b5 | ||
|
|
c3d36b29b0 | ||
|
|
1b69a2ff52 | ||
|
|
311497d080 | ||
|
|
55b7739d36 | ||
|
|
0fda97eb2f | ||
|
|
879815fc02 | ||
|
|
c469c3e856 | ||
|
|
1b6750e0c5 | ||
|
|
f14bd5c041 | ||
|
|
76115a3e9c | ||
|
|
65f86e0177 | ||
|
|
18cacb1532 | ||
|
|
6a924ac88b | ||
|
|
d125af6775 | ||
|
|
1a9a0579a5 | ||
|
|
acee131288 | ||
|
|
66536a5260 | ||
|
|
7941a83109 | ||
|
|
b312b80b69 | ||
|
|
b3c3b73b7d | ||
|
|
0a8e9cde1b | ||
|
|
ca3f48697d | ||
|
|
067e5cd5e2 | ||
|
|
7e16c8c1f2 | ||
|
|
e932aad47b | ||
|
|
2eb8bb3cf5 |
@@ -0,0 +1,210 @@
|
||||
step-restore-cache: &step-restore-cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v1-modules-{{ arch }}-{{ checksum "yarn.lock" }}
|
||||
- v1-modules-{{ arch }}
|
||||
step-install: &step-install
|
||||
run: npx yarn --frozen-lockfile && rm -rf node_modules/electron/dist
|
||||
step-save-cache: &step-save-cache
|
||||
save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-modules-{{ arch }}-{{ checksum "yarn.lock" }}
|
||||
step-install-electron: &step-install-electron
|
||||
run: cd node_modules/electron && node install.js
|
||||
|
||||
steps-test: &steps-test
|
||||
steps:
|
||||
- checkout
|
||||
- *step-restore-cache
|
||||
- *step-install
|
||||
- *step-save-cache
|
||||
- *step-install-electron
|
||||
- run: yarn lint
|
||||
- run: yarn test --maxWorkers=4
|
||||
|
||||
steps-build: &steps-build
|
||||
steps:
|
||||
- checkout
|
||||
- *step-restore-cache
|
||||
- *step-install
|
||||
- *step-save-cache
|
||||
- *step-install-electron
|
||||
- when:
|
||||
condition: <<parameters.on_linux>>
|
||||
steps:
|
||||
- run:
|
||||
name: Install Linux Dependencies
|
||||
command: sudo apt install -y rpm fakeroot
|
||||
- when:
|
||||
condition: <<parameters.on_mac>>
|
||||
steps:
|
||||
- run:
|
||||
name: Import macOS Certificate
|
||||
command: .circleci/import-macos-cert.sh
|
||||
- when:
|
||||
condition: <<parameters.on_win>>
|
||||
steps:
|
||||
- run:
|
||||
name: Import Windows Certificate
|
||||
command: |
|
||||
if (Test-Path Env:\WINDOWS_CERTIFICATE_P12) {
|
||||
$workingDirectory = Convert-Path (Resolve-Path -path ".")
|
||||
$filename = "$workingDirectory\cert.p12"
|
||||
$bytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_P12)
|
||||
[IO.File]::WriteAllBytes($filename, $bytes)
|
||||
}
|
||||
shell: powershell.exe
|
||||
- run:
|
||||
name: Publish dry run
|
||||
command: yarn run publish --dry-run --arch=$TARGET_ARCH
|
||||
- store_artifacts:
|
||||
path: out/make
|
||||
destination: dist
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- out/make
|
||||
- out/publish-dry-run
|
||||
|
||||
version: 2.1
|
||||
orbs:
|
||||
win: circleci/windows@1.0.0
|
||||
jobs:
|
||||
test-linux:
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
<<: *steps-test
|
||||
build-linux:
|
||||
docker:
|
||||
- image: circleci/node:10-buster
|
||||
environment:
|
||||
TARGET_ARCH: x64
|
||||
parameters:
|
||||
on_linux:
|
||||
type: boolean
|
||||
default: true
|
||||
on_mac:
|
||||
type: boolean
|
||||
default: false
|
||||
on_win:
|
||||
type: boolean
|
||||
default: false
|
||||
<<: *steps-build
|
||||
|
||||
test-mac:
|
||||
macos:
|
||||
xcode: "10.2.0"
|
||||
<<: *steps-test
|
||||
build-mac:
|
||||
macos:
|
||||
xcode: "10.2.0"
|
||||
environment:
|
||||
TARGET_ARCH: x64
|
||||
parameters:
|
||||
on_linux:
|
||||
type: boolean
|
||||
default: false
|
||||
on_mac:
|
||||
type: boolean
|
||||
default: true
|
||||
on_win:
|
||||
type: boolean
|
||||
default: false
|
||||
<<: *steps-build
|
||||
|
||||
test-windows:
|
||||
executor:
|
||||
name: win/vs2019
|
||||
shell: bash.exe
|
||||
<<: *steps-test
|
||||
build-windows-ia32:
|
||||
executor:
|
||||
name: win/vs2019
|
||||
shell: bash.exe
|
||||
environment:
|
||||
TARGET_ARCH: ia32
|
||||
parameters:
|
||||
on_linux:
|
||||
type: boolean
|
||||
default: false
|
||||
on_mac:
|
||||
type: boolean
|
||||
default: false
|
||||
on_win:
|
||||
type: boolean
|
||||
default: true
|
||||
<<: *steps-build
|
||||
build-windows-x64:
|
||||
executor:
|
||||
name: win/vs2019
|
||||
shell: bash.exe
|
||||
environment:
|
||||
TARGET_ARCH: x64
|
||||
parameters:
|
||||
on_linux:
|
||||
type: boolean
|
||||
default: false
|
||||
on_mac:
|
||||
type: boolean
|
||||
default: false
|
||||
on_win:
|
||||
type: boolean
|
||||
default: true
|
||||
<<: *steps-build
|
||||
|
||||
finalize:
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
steps:
|
||||
- checkout
|
||||
- *step-restore-cache
|
||||
- *step-install
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- store_artifacts:
|
||||
path: out/make
|
||||
destination: dist
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
steps:
|
||||
- checkout
|
||||
- *step-restore-cache
|
||||
- *step-install
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run: yarn publish --from-dry-run
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- test-linux
|
||||
- test-mac
|
||||
- test-windows
|
||||
|
||||
- build-linux
|
||||
- build-mac
|
||||
- build-windows-ia32
|
||||
- build-windows-x64
|
||||
|
||||
- finalize:
|
||||
requires:
|
||||
- build-linux
|
||||
- build-mac
|
||||
- build-windows-ia32
|
||||
- build-windows-x64
|
||||
- release:
|
||||
requires:
|
||||
- build-linux
|
||||
- build-mac
|
||||
- build-windows-ia32
|
||||
- build-windows-x64
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /v.*/
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [[ ! -z $MACOS_CERT_P12 ]]
|
||||
then
|
||||
|
||||
export CERTIFICATE_P12=cert.p12;
|
||||
echo $MACOS_CERT_P12 | base64 --decode > $CERTIFICATE_P12;
|
||||
export KEYCHAIN=build.keychain;
|
||||
|
||||
# Create the keychain with a password
|
||||
security create-keychain -p travis $KEYCHAIN;
|
||||
|
||||
# Make the custom keychain default, so xcodebuild will use it for signing
|
||||
security default-keychain -s $KEYCHAIN;
|
||||
|
||||
# Unlock the keychain
|
||||
security unlock-keychain -p travis $KEYCHAIN;
|
||||
|
||||
# Add certificates to keychain and allow codesign to access them
|
||||
# Apple Worldwide Developer Relations Certification Authority
|
||||
security import ./tools/certs/apple.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
|
||||
# Developer Authentication Certification Authority
|
||||
security import ./tools/certs/dac.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
|
||||
# Developer ID Felix
|
||||
security import $CERTIFICATE_P12 -k $KEYCHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign 2>&1 >/dev/null;
|
||||
rm $CERTIFICATE_P12;
|
||||
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k travis $KEYCHAIN
|
||||
|
||||
# Echo the identity
|
||||
security find-identity -v -p codesigning
|
||||
fi
|
||||
@@ -0,0 +1,31 @@
|
||||
const { Octokit } = require('@octokit/action')
|
||||
|
||||
const octokit = new Octokit()
|
||||
|
||||
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
|
||||
|
||||
async function main () {
|
||||
const prs = await octokit.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
head: `${owner}:update-releases`
|
||||
})
|
||||
|
||||
if (prs.data.length === 0) {
|
||||
const pr = await octokit.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title: 'build: update Electron releases JSON',
|
||||
base: 'master',
|
||||
head: 'update-releases',
|
||||
body: 'Auto-update from GitHub Actions.',
|
||||
maintainer_can_modify: true
|
||||
})
|
||||
console.log('Pull request created:', pr.html_url)
|
||||
} else {
|
||||
console.log('Pull request updated:', prs.data[0].html_url)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Auto-update Releases JSON file
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
jobs:
|
||||
autoupdate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Fetch git branches
|
||||
run: git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Get npm cache directory
|
||||
id: npm-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(npm config get cache)"
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.npm-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- run: npm ci
|
||||
- name: Switch to release update branch
|
||||
run: |
|
||||
if git branch --remotes | grep -q origin/update-releases; then
|
||||
git checkout update-releases
|
||||
else
|
||||
git checkout -b update-releases
|
||||
fi
|
||||
- name: Update Releases JSON
|
||||
run: npm run electron-releases
|
||||
- name: Commit Changes to Releases JSON
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "machine github.com login $GITHUB_ACTOR password $GITHUB_TOKEN" > ~/.netrc
|
||||
chmod 600 ~/.netrc
|
||||
git add static/releases.json tests/fixtures/releases-metadata.json
|
||||
if test -n "$(git status -s)"; then
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "electron-bot@users.noreply.github.com"
|
||||
git diff --cached
|
||||
git commit -m "build: update Electron releases JSON"
|
||||
git push origin update-releases
|
||||
node --unhandled-rejections=strict .github/actions/create_releases_pr.js
|
||||
else
|
||||
echo No update needed
|
||||
fi
|
||||
+3
-1
@@ -21,7 +21,6 @@
|
||||
/out/
|
||||
node_modules/
|
||||
SHASUMS256.txt
|
||||
**/yarn.lock
|
||||
compile_commands.json
|
||||
.envrc
|
||||
.cache
|
||||
@@ -31,6 +30,9 @@ yarn-error.log
|
||||
# Coverage
|
||||
coverage
|
||||
|
||||
# Test report
|
||||
report.json
|
||||
|
||||
# npm package
|
||||
/npm/dist
|
||||
/npm/path.txt
|
||||
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
language: node_js
|
||||
node_js: "8"
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
dist: trusty
|
||||
osx_image: xcode8.3
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- $HOME/.cache/electron
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- fakeroot
|
||||
- rpm
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+/
|
||||
|
||||
install:
|
||||
- npm install
|
||||
|
||||
script:
|
||||
- npm run lint
|
||||
- npm run test -- --runInBand
|
||||
- if test -z "$TRAVIS_TAG"; then npm run make; fi
|
||||
after_success: if test -n "$TRAVIS_TAG"; then npm run publish; fi
|
||||
@@ -0,0 +1,74 @@
|
||||
# Contributing to Electron Fiddle
|
||||
|
||||
Electron Fiddle is a community-driven project, overseen by the [Electron Ecosystem Working
|
||||
Group](https://github.com/electron/governance/tree/master/wg-ecosystem#readme). As such, we welcome
|
||||
and encourage all sorts of contributions. They include, but are not limited to:
|
||||
|
||||
- Constructive feedback
|
||||
- Bug reports / technical issues
|
||||
- Documentation changes
|
||||
- Feature requests
|
||||
- [Pull requests](#filing-pull-requests)
|
||||
|
||||
We strongly suggest that before filing an issue, you search through the existing issues to see
|
||||
if it has already been filed by someone else.
|
||||
|
||||
This project is a part of the Electron ecosystem. As such, all contributions to this project follow
|
||||
[Electron's code of conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md)
|
||||
where appropriate.
|
||||
|
||||
## Contribution Suggestions
|
||||
|
||||
We use the label [`good first issue`](https://github.com/electron/fiddle/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) in the issue tracker to denote fairly-well-scoped-out bugs or feature requests that the community can pick up and work on. If any of those labeled issues do not have enough information, please feel free to ask constructive questions. (This applies to any open issue.)
|
||||
|
||||
## Filing Pull Requests
|
||||
|
||||
Here are some things to keep in mind as you file pull requests to fix bugs, add new features, etc.:
|
||||
|
||||
* If you're unfamiliar with forking, branching, and pull requests, please see [GitHub's extensive
|
||||
documentation](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests).
|
||||
* Travis CI and AppVeyor are used to make sure that any new or changed code meets the project's
|
||||
style guidelines, and that the project's testsuite passes for each new commit.
|
||||
* Unless it's impractical, please write tests for your changes. This will help us so that we can
|
||||
spot regressions much easier.
|
||||
* When creating commit messages and pull request titles, please adhere to the [conventional
|
||||
commits](https://www.conventionalcommits.org/en/v1.0.0/) standard.
|
||||
* Please **do not** bump the version number in your pull requests, the maintainers will do that.
|
||||
Feel free to indicate whether the changes require a major, minor, or patch version bump, as
|
||||
prescribed by the [semantic versioning specification](http://semver.org/).
|
||||
|
||||
### Running Fiddle From Source
|
||||
|
||||
1. Clone the Electron Fiddle repository locally.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/electron/fiddle.git
|
||||
```
|
||||
|
||||
2. Change directory to where Fiddle has been cloned.
|
||||
|
||||
```sh
|
||||
cd fiddle
|
||||
```
|
||||
|
||||
3. Install dependencies.
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Start Fiddle and Explore!
|
||||
|
||||
```sh
|
||||
npm start
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```sh
|
||||
npm test
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
<!-- TODO @felix add your release process here 😁 -->
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2013-2018 GitHub Inc.
|
||||
Copyright (c) 2013-2020 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# <img src="https://user-images.githubusercontent.com/378023/49785546-4b7f7000-fd64-11e8-8033-a52c73a07fbf.png" width="60px" align="center" alt="Electron Fiddle icon"> Electron Fiddle
|
||||
|
||||
[](https://travis-ci.org/electron/fiddle)
|
||||
[](https://travis-ci.org/electron/fiddle) [](https://coveralls.io/github/electron/fiddle?branch=master)
|
||||
|
||||
Electron Fiddle lets you create and play with small Electron experiments. It
|
||||
greets you with a quick-start template after opening – change a few things,
|
||||
choose the version of Electron you want to run it with, and play around. Then,
|
||||
save your Fiddle either as a GitHub Gist or to a local folder. Once pushed to
|
||||
GitHub, anyone can quickly try your Fiddle out by just entering it in the
|
||||
save your Fiddle either as a GitHub Gist or to a local folder. Once published
|
||||
on GitHub, anyone can quickly try your Fiddle out by just entering it in the
|
||||
address bar.
|
||||
|
||||
<h3 align="center">
|
||||
@@ -15,13 +15,13 @@ address bar.
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/378023/49716908-7d2b0500-fc98-11e8-8f52-def504802509.png" width="880px" alt="Electron Fiddle screenshot">
|
||||
<img src="https://user-images.githubusercontent.com/1426799/52155868-d3357c80-2639-11e9-9496-fa97b1dc7897.jpg" width="880px" alt="Electron Fiddle screenshot">
|
||||
|
||||
# Features
|
||||
|
||||
### Explore Electron
|
||||
|
||||

|
||||

|
||||
|
||||
Try Electron without installing any dependencies: Fiddle includes everything
|
||||
you'll need to explore the platform. It also includes examples for every API
|
||||
@@ -40,7 +40,7 @@ all Electron APIs only a few keystrokes away.
|
||||
|
||||
### Compile and Package
|
||||
|
||||

|
||||

|
||||
|
||||
Fiddle can automatically turn your experiment into binaries you can share with
|
||||
your friends, coworkers, or grandparents. It does so thanks to
|
||||
@@ -56,6 +56,10 @@ fiddle has grown up, export it as a project with or without
|
||||
[electron-forge][electron-forge]. Then, use your favorite editor and take on
|
||||
the world!
|
||||
|
||||
## Contributing
|
||||
|
||||
Electron Fiddle is a community-driven project that welcomes all sorts of contributions. Please check out our [Contributing Guide](https://github.com/electron/fiddle/blob/master/CONTRIBUTING.md) for more details.
|
||||
|
||||
## License
|
||||
|
||||
[MIT, please see the LICENSE file for full details](https://github.com/electron/fiddle/blob/master/LICENSE.md).
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "8"
|
||||
|
||||
init:
|
||||
- git config --global core.symlinks true
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- node --version
|
||||
- npm ci
|
||||
|
||||
cache:
|
||||
- '%APPDATA%\npm-cache -> appveyor.yml'
|
||||
|
||||
test_script:
|
||||
- npm run lint
|
||||
- npm run test
|
||||
|
||||
build: off
|
||||
+65
-13
@@ -1,12 +1,13 @@
|
||||
/* tslint:disable */
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const { version } = packageJson;
|
||||
const iconDir = path.resolve(__dirname, 'assets', 'icons');
|
||||
const { version } = packageJson
|
||||
const iconDir = path.resolve(__dirname, 'assets', 'icons')
|
||||
|
||||
module.exports = {
|
||||
const config = {
|
||||
hooks: {
|
||||
generateAssets: require('./tools/generateAssets')
|
||||
},
|
||||
@@ -15,12 +16,11 @@ module.exports = {
|
||||
executableName: 'electron-fiddle',
|
||||
asar: true,
|
||||
icon: path.resolve(__dirname, 'assets', 'icons', 'fiddle'),
|
||||
// TODO: FIXME?
|
||||
// ignore: [
|
||||
// /^\/\.vscode\//,
|
||||
// /^\/tools\//
|
||||
// ],
|
||||
appBundleId: 'com.electron.fiddle',
|
||||
usageDescription: {
|
||||
Camera: 'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Camera',
|
||||
Microphone: 'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Microphone'
|
||||
},
|
||||
appCategoryType: 'public.app-category.developer-tools',
|
||||
protocols: [{
|
||||
name: 'Electron Fiddle Launch Protocol',
|
||||
@@ -31,14 +31,27 @@ module.exports = {
|
||||
OriginalFilename: 'Electron Fiddle',
|
||||
},
|
||||
osxSign: {
|
||||
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)'
|
||||
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
|
||||
'hardened-runtime': true,
|
||||
'gatekeeper-assess': false,
|
||||
'entitlements': 'static/entitlements.plist',
|
||||
'entitlements-inherit': 'static/entitlements.plist',
|
||||
'signature-flags': 'library'
|
||||
}
|
||||
},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
platforms: ['win32'],
|
||||
config: arch => {
|
||||
config: (arch) => {
|
||||
const certificateFile = process.env.CI
|
||||
? path.join(__dirname, 'cert.p12')
|
||||
: process.env.WINDOWS_CERTIFICATE_FILE;
|
||||
|
||||
if (!certificateFile || !fs.existsSync(certificateFile)) {
|
||||
console.warn(`Warning: Could not find certificate file at ${certificateFile}`)
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'electron-fiddle',
|
||||
authors: 'Electron Community',
|
||||
@@ -49,8 +62,8 @@ module.exports = {
|
||||
remoteReleases: '',
|
||||
setupExe: `electron-fiddle-${version}-${arch}-setup.exe`,
|
||||
setupIcon: path.resolve(iconDir, 'fiddle.ico'),
|
||||
certificateFile: process.env.WINDOWS_CERTIFICATE_FILE,
|
||||
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD
|
||||
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD,
|
||||
certificateFile
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -80,8 +93,47 @@ module.exports = {
|
||||
owner: 'electron',
|
||||
name: 'fiddle'
|
||||
},
|
||||
prerelease: true
|
||||
draft: true,
|
||||
prerelease: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function notarizeMaybe() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.npm_lifecycle_event !== 'publish') {
|
||||
console.log(`Not in "publish" mode, skipping notarization`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.CI) {
|
||||
console.log(`Not in CI, skipping notarization`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.TRAVIS_TAG && !process.env.NOTARIZE_WITHOUT_TAG) {
|
||||
console.log(`Not a tag, not notarizing`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
|
||||
console.warn('Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!');
|
||||
return;
|
||||
}
|
||||
|
||||
config.packagerConfig.osxNotarize = {
|
||||
appBundleId: 'com.electron.fiddle',
|
||||
appleId: process.env.APPLE_ID,
|
||||
appleIdPassword: process.env.APPLE_ID_PASSWORD,
|
||||
ascProvider: 'LT94ZKYDCJ'
|
||||
}
|
||||
}
|
||||
|
||||
notarizeMaybe()
|
||||
|
||||
// Finally, export it
|
||||
module.exports = config
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
{
|
||||
"transform": {
|
||||
".(js|jsx|ts|tsx)": "ts-jest"
|
||||
"\\.(js|jsx|ts|tsx)$": "ts-jest"
|
||||
},
|
||||
"testURL": "http://localhost",
|
||||
"testRegex": "(-spec)\\.(ts|tsx)$",
|
||||
"bail": true,
|
||||
"resetMocks": true,
|
||||
"bail": true,
|
||||
"resetModules": true,
|
||||
"setupTestFrameworkScriptFile": "<rootDir>/tests/setup.js",
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/tests/setup.js"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"jsx",
|
||||
@@ -20,11 +25,13 @@
|
||||
],
|
||||
"coverageReporters": [
|
||||
"json",
|
||||
"html"
|
||||
"html",
|
||||
"lcov"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"!**/*.d.ts"
|
||||
"!**/*.d.ts",
|
||||
"!**/*constants.ts"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
|
||||
Generated
-16992
File diff suppressed because it is too large
Load Diff
+74
-66
@@ -1,27 +1,30 @@
|
||||
{
|
||||
"name": "electron-fiddle",
|
||||
"productName": "Electron Fiddle",
|
||||
"version": "0.4.0",
|
||||
"version": "0.15.1",
|
||||
"description": "The easiest way to get started with Electron",
|
||||
"repository": "https://github.com/electron/fiddle",
|
||||
"main": "./dist/src/main/main",
|
||||
"scripts": {
|
||||
"contributors": "node ./tools/contributors.js",
|
||||
"less": "node ./tools/lessc.js",
|
||||
"lint:style": "stylelint ./src/less/*.less --fix",
|
||||
"lint:style": "stylelint \"./src/less/*.less\" --fix",
|
||||
"lint:ts": "tslint -c tslint.json -p tsconfig.json -e \"node_modules/**/*.ts\" --fix",
|
||||
"lint:tests": "tslint ./tests/**/*.ts{,x} -c tslint.json --fix",
|
||||
"lint:templates": "standard ./static/templates/**/*.js",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:tests": "tslint \"./tests/**/*.ts{,x}\" -c tslint.json --fix",
|
||||
"lint:templates": "standard \"./static/show-me/**/*.js\"",
|
||||
"lint": "npm-run-all \"lint:*\"",
|
||||
"make": "electron-forge make",
|
||||
"package": "electron-forge package",
|
||||
"parcel:build": "node ./tools/parcel-build.js",
|
||||
"parcel:watch": "node ./tools/parcel-watch.js",
|
||||
"publish": "electron-forge publish",
|
||||
"start": "rimraf ./dist && electron-forge start",
|
||||
"test": "jest --config=jest.json --coverage",
|
||||
"tsc": "node ./tools/tsc.js",
|
||||
"electron-releases": "node ./tools/fetch-releases.js"
|
||||
"test": "jest --config=jest.json",
|
||||
"test:ci": "jest --config=jest.json --coverage --runInBand",
|
||||
"test:coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
|
||||
"test:report": "jest --config=jest.json --json --bail=false --outputFile=report.json | true",
|
||||
"tsc": "tsc --noEmit -p .",
|
||||
"electron-releases": "node --unhandled-rejections=strict ./tools/fetch-releases.js"
|
||||
},
|
||||
"keywords": [
|
||||
"Electron",
|
||||
@@ -34,68 +37,73 @@
|
||||
"forge": "./forge.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^3.10.0",
|
||||
"@blueprintjs/select": "^3.4.0",
|
||||
"@octokit/rest": "^15.12.1",
|
||||
"builtin-modules": "^3.0.0",
|
||||
"@blueprintjs/core": "^3.24.0",
|
||||
"@blueprintjs/select": "^3.12.0",
|
||||
"@electron/get": "^1.9.0",
|
||||
"@octokit/rest": "^16.43.1",
|
||||
"@sentry/electron": "^1.2.1",
|
||||
"classnames": "^2.2.6",
|
||||
"electron-default-menu": "^1.0.1",
|
||||
"electron-download": "^4.1.1",
|
||||
"electron-devtools-installer": "^2.2.4",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"extract-zip": "^1.6.7",
|
||||
"fix-path": "^2.1.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"loader-utils": "^1.1.0",
|
||||
"mobx": "^5.6.0",
|
||||
"mobx-react": "^5.4.2",
|
||||
"monaco-loader": "^0.14.0",
|
||||
"namor": "^1.1.1",
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-mosaic-component": "^1.1.1",
|
||||
"semver": "^5.6.0",
|
||||
"tmp": "0.0.33",
|
||||
"tslib": "^1.9.3",
|
||||
"update-electron-app": "^1.3.0"
|
||||
"fix-path": "^3.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"mobx": "^5.15.4",
|
||||
"mobx-react": "^6.1.8",
|
||||
"monaco-editor": "^0.20.0",
|
||||
"monaco-loader": "1.0.0",
|
||||
"namor": "^2.0.2",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-mosaic-component": "^3.2.0",
|
||||
"semver": "^7.1.3",
|
||||
"tmp": "0.1.0",
|
||||
"tslib": "^1.11.1",
|
||||
"update-electron-app": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.0.0-beta.30",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.30",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.30",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.30",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.30",
|
||||
"@types/builtin-modules": "^2.0.0",
|
||||
"@types/enzyme": "^3.1.13",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/jest": "^23.3.2",
|
||||
"@types/log-symbols": "^2.0.0",
|
||||
"@types/node": "^10.11.1",
|
||||
"@types/react": "^16.4.14",
|
||||
"@types/react-dom": "^16.0.8",
|
||||
"@types/semver": "^5.5.0",
|
||||
"@types/tmp": "0.0.33",
|
||||
"chokidar": "^2.0.4",
|
||||
"electron": "3.0.10",
|
||||
"enzyme": "^3.6.0",
|
||||
"enzyme-adapter-react-16": "^1.5.0",
|
||||
"enzyme-to-json": "^3.3.4",
|
||||
"jest": "^23.6.0",
|
||||
"jest-fetch-mock": "^1.6.6",
|
||||
"less": "^3.8.1",
|
||||
"log-symbols": "^2.2.0",
|
||||
"monaco-editor": "^0.15.6",
|
||||
"node-abi": "^2.5.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"parcel-bundler": "^1.10.3",
|
||||
"react-test-renderer": "^16.4.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"standard": "^12.0.1",
|
||||
"stylelint": "^9.6.0",
|
||||
"stylelint-config-standard": "^18.2.0",
|
||||
"ts-jest": "^23.10.5",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-microsoft-contrib": "^5.2.1",
|
||||
"tslint-react": "^3.6.0",
|
||||
"typescript": "^3.1.6"
|
||||
"@babel/core": "^7.8.6",
|
||||
"@electron-forge/cli": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.52",
|
||||
"@electron-forge/publisher-github": "^6.0.0-beta.52",
|
||||
"@octokit/action": "^2.0.0",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/enzyme": "^3.10.5",
|
||||
"@types/fs-extra": "^8.1.0",
|
||||
"@types/jest": "^25.1.3",
|
||||
"@types/log-symbols": "^3.0.0",
|
||||
"@types/node": "^13.7.6",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/semver": "^7.1.0",
|
||||
"@types/tmp": "0.1.0",
|
||||
"chokidar": "^3.3.1",
|
||||
"coveralls": "^3.0.9",
|
||||
"electron": "8.0.2",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"enzyme-to-json": "^3.4.4",
|
||||
"fetch-mock-jest": "^1.1.0-beta.3",
|
||||
"jest": "^25.1.0",
|
||||
"less": "^3.11.1",
|
||||
"log-symbols": "^3.0.0",
|
||||
"node-abi": "^2.15.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"react-test-renderer": "^16.13.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"standard": "^14.3.1",
|
||||
"stylelint": "^13.2.0",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"tslint": "^6.0.0",
|
||||
"tslint-microsoft-contrib": "^6.2.0",
|
||||
"tslint-react": "^4.2.0",
|
||||
"typescript": "^3.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as electronModule from 'electron';
|
||||
import * as path from 'path';
|
||||
|
||||
const electron = electronModule.remote
|
||||
? electronModule.remote
|
||||
: electronModule;
|
||||
|
||||
export const USER_DATA_PATH = electron.app.getPath('userData');
|
||||
export const INDEX_HTML_NAME = 'index.html';
|
||||
export const MAIN_JS_NAME = 'main.js';
|
||||
export const RENDERER_JS_NAME = 'renderer.js';
|
||||
export const PACKAGE_NAME = 'package.json';
|
||||
|
||||
export const CONFIG_PATH = path.join(electron.app.getPath('home'), '.electron-fiddle');
|
||||
@@ -3,6 +3,7 @@ export const html = `<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hello World!</title>
|
||||
<link rel="stylesheet" type="text/css" href="./styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World!</h1>
|
||||
|
||||
@@ -3,13 +3,9 @@ const {app, BrowserWindow} = require('electron')
|
||||
const path = require('path')
|
||||
const url = require('url')
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow
|
||||
|
||||
function createWindow () {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({width: 800, height: 600})
|
||||
const mainWindow = new BrowserWindow({width: 800, height: 600})
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadURL(url.format({
|
||||
@@ -20,14 +16,6 @@ function createWindow () {
|
||||
|
||||
// Open the DevTools.
|
||||
// mainWindow.webContents.openDevTools()
|
||||
|
||||
// Emitted when the window is closed.
|
||||
mainWindow.on('closed', function () {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
mainWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -47,7 +35,7 @@ app.on('window-all-closed', function () {
|
||||
app.on('activate', function () {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
+11
-17
@@ -1,27 +1,21 @@
|
||||
export const main = `// Modules to control application life and create native browser window
|
||||
const {app, BrowserWindow} = require('electron')
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow
|
||||
|
||||
function createWindow () {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({width: 800, height: 600})
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
})
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile('index.html')
|
||||
|
||||
// Open the DevTools.
|
||||
// mainWindow.webContents.openDevTools()
|
||||
|
||||
// Emitted when the window is closed.
|
||||
mainWindow.on('closed', function () {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
mainWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -29,10 +23,10 @@ function createWindow () {
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow)
|
||||
|
||||
// Quit when all windows are closed.
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', function () {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
@@ -41,7 +35,7 @@ app.on('window-all-closed', function () {
|
||||
app.on('activate', function () {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
+48
-8
@@ -4,17 +4,18 @@ export type Files = Map<string, string>;
|
||||
|
||||
export type FileTransform = (files: Files) => Promise<Files>;
|
||||
|
||||
export enum ElectronVersionState {
|
||||
export enum VersionState {
|
||||
ready = 'ready',
|
||||
downloading = 'downloading',
|
||||
unzipping = 'unzipping',
|
||||
unknown = 'unknown'
|
||||
}
|
||||
|
||||
export enum ElectronVersionSource {
|
||||
export enum VersionSource {
|
||||
remote = 'remote',
|
||||
local = 'local'
|
||||
}
|
||||
export interface NpmVersion {
|
||||
export interface Version {
|
||||
version: string;
|
||||
name?: string;
|
||||
localPath?: string;
|
||||
@@ -24,12 +25,21 @@ export interface EditorValues {
|
||||
main: string;
|
||||
renderer: string;
|
||||
html: string;
|
||||
preload: string;
|
||||
css: string;
|
||||
package?: string;
|
||||
}
|
||||
|
||||
export interface ElectronVersion extends NpmVersion {
|
||||
state: ElectronVersionState;
|
||||
source: ElectronVersionSource;
|
||||
export interface RunnableVersion extends Version {
|
||||
state: VersionState;
|
||||
source: VersionSource;
|
||||
downloadProgress?: number;
|
||||
}
|
||||
|
||||
export interface SetFiddleOptions {
|
||||
filePath?: string;
|
||||
templateName?: string;
|
||||
gistId?: string;
|
||||
}
|
||||
|
||||
export interface OutputEntry {
|
||||
@@ -43,7 +53,8 @@ export interface OutputOptions {
|
||||
isNotPre?: boolean;
|
||||
}
|
||||
|
||||
export interface WarningDialogTexts {
|
||||
export interface GenericDialogOptions {
|
||||
type: GenericDialogType;
|
||||
ok?: string;
|
||||
cancel?: string;
|
||||
label: string;
|
||||
@@ -53,6 +64,35 @@ export interface Templates {
|
||||
[index: string]: string | Templates;
|
||||
}
|
||||
|
||||
export type EditorId = 'main' | 'renderer' | 'html';
|
||||
export const enum GenericDialogType {
|
||||
'confirm' = 'confirm',
|
||||
'warning' = 'warning',
|
||||
'success' = 'success',
|
||||
}
|
||||
|
||||
// Editors
|
||||
export const enum EditorId {
|
||||
'main' = 'main',
|
||||
'renderer' = 'renderer',
|
||||
'html' = 'html',
|
||||
'preload' = 'preload',
|
||||
'css' = 'css'
|
||||
}
|
||||
|
||||
// Panels that can show up as a mosaic
|
||||
export const enum PanelId {
|
||||
'docsDemo' = 'docsDemo'
|
||||
}
|
||||
|
||||
export type MosaicId = EditorId | PanelId;
|
||||
|
||||
export const ALL_EDITORS = [EditorId.main, EditorId.renderer, EditorId.preload, EditorId.html, EditorId.css];
|
||||
export const ALL_PANELS = [PanelId.docsDemo];
|
||||
export const ALL_MOSAICS = [...ALL_EDITORS, ...ALL_PANELS];
|
||||
|
||||
export type ArrowPosition = 'top' | 'left' | 'bottom' | 'right';
|
||||
|
||||
export const enum DocsDemoPage {
|
||||
DEFAULT = 'DEFAULT',
|
||||
DEMO_APP = 'DEMO_APP'
|
||||
}
|
||||
|
||||
+25
-5
@@ -1,10 +1,12 @@
|
||||
export enum IpcEvents {
|
||||
OPEN_SETTINGS = 'OPEN_SETTINGS',
|
||||
LOAD_GIST_REQUEST = 'LOAD_GIST_REQUEST',
|
||||
LOAD_ELECTRON_EXAMPLE_REQUEST = 'LOAD_ELECTRON_EXAMPLE_REQUEST',
|
||||
FIDDLE_RUN = 'FIDDLE_RUN',
|
||||
FIDDLE_PACKAGE = 'FIDDLE_PACKAGE',
|
||||
FIDDLE_MAKE = 'FIDDLE_MAKE',
|
||||
MONACO_EXECUTE_COMMAND = 'MONACO_EXECUTE_COMMAND',
|
||||
MONACO_TOGGLE_OPTION = 'MONACO_TOGGLE_OPTION',
|
||||
FS_NEW_FIDDLE = 'FS_NEW_FIDDLE',
|
||||
FS_OPEN_FIDDLE = 'FS_OPEN_FIDDLE',
|
||||
FS_OPEN_TEMPLATE = 'FS_OPEN_TEMPLATE',
|
||||
@@ -15,24 +17,37 @@ export enum IpcEvents {
|
||||
FS_SAVE_FIDDLE_DIALOG = 'FS_SAVE_FIDDLE_DIALOG',
|
||||
FS_SAVE_FIDDLE_ERROR = 'FS_SAVE_FIDDLE_ERROR',
|
||||
SHOW_WARNING_DIALOG = 'SHOW_WARNING_DIALOG',
|
||||
SHOW_CONFIRMATION_DIALOG = 'SHOW_CONFIRMATION_DIALOG',
|
||||
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR',
|
||||
TOGGLE_SOFT_WRAP = 'TOGGLE_SOFT_WRAP',
|
||||
TOGGLE_MINI_MAP = 'TOGGLE_MINI_MAP'
|
||||
CLEAR_CONSOLE = 'CLEAR_CONSOLE',
|
||||
LOAD_LOCAL_VERSION_FOLDER = 'LOAD_LOCAL_VERSION_FOLDER',
|
||||
SHOW_LOCAL_VERSION_FOLDER_DIALOG = 'SHOW_LOCAL_VERSION_FOLDER_DIALOG',
|
||||
BISECT_COMMANDS_TOGGLE = 'BISECT_COMMANDS_TOGGLE',
|
||||
SHOW_INACTIVE = 'SHOW_INACTIVE',
|
||||
BEFORE_QUIT = 'BEFORE_QUIT',
|
||||
CONFIRM_QUIT = 'CONFIRM_QUIT',
|
||||
SET_APPDATA_DIR = 'SET_APPDATA_DIR',
|
||||
}
|
||||
|
||||
export const ipcMainEvents = [
|
||||
IpcEvents.FS_SAVE_FIDDLE_DIALOG,
|
||||
IpcEvents.FS_SAVE_FIDDLE,
|
||||
IpcEvents.SHOW_WARNING_DIALOG
|
||||
IpcEvents.SHOW_WARNING_DIALOG,
|
||||
IpcEvents.SHOW_CONFIRMATION_DIALOG,
|
||||
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG,
|
||||
IpcEvents.SHOW_INACTIVE,
|
||||
IpcEvents.CONFIRM_QUIT,
|
||||
];
|
||||
|
||||
export const ipcRendererEvents = [
|
||||
IpcEvents.OPEN_SETTINGS,
|
||||
IpcEvents.LOAD_GIST_REQUEST,
|
||||
IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST,
|
||||
IpcEvents.FIDDLE_RUN,
|
||||
IpcEvents.FIDDLE_PACKAGE,
|
||||
IpcEvents.FIDDLE_MAKE,
|
||||
IpcEvents.MONACO_EXECUTE_COMMAND,
|
||||
IpcEvents.MONACO_TOGGLE_OPTION,
|
||||
IpcEvents.FS_NEW_FIDDLE,
|
||||
IpcEvents.FS_OPEN_FIDDLE,
|
||||
IpcEvents.FS_OPEN_TEMPLATE,
|
||||
@@ -41,6 +56,11 @@ export const ipcRendererEvents = [
|
||||
IpcEvents.FS_SAVE_FIDDLE_FORGE,
|
||||
IpcEvents.FS_SAVE_FIDDLE_GIST,
|
||||
IpcEvents.SHOW_WELCOME_TOUR,
|
||||
IpcEvents.TOGGLE_SOFT_WRAP,
|
||||
IpcEvents.TOGGLE_MINI_MAP
|
||||
IpcEvents.CLEAR_CONSOLE,
|
||||
IpcEvents.LOAD_LOCAL_VERSION_FOLDER,
|
||||
IpcEvents.BISECT_COMMANDS_TOGGLE,
|
||||
IpcEvents.BEFORE_QUIT,
|
||||
IpcEvents.SET_APPDATA_DIR,
|
||||
];
|
||||
|
||||
export const WEBCONTENTS_READY_FOR_IPC_SIGNAL = 'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
|
||||
|
||||
@@ -18,6 +18,15 @@
|
||||
background-color: @background-1;
|
||||
}
|
||||
|
||||
.bp3-button.bp3-minimal {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.bp3-button:hover,
|
||||
.bp3-button.bp3-minimal:hover {
|
||||
background-color: rgba(138, 155, 168, 0.15);
|
||||
}
|
||||
|
||||
.bp3-menu-item.bp3-active.bp3-intent-primary {
|
||||
background-color: @foreground-3;
|
||||
}
|
||||
@@ -29,4 +38,8 @@
|
||||
background-color: @background-3;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-running-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
button.version-chooser.bp3-button {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
header {
|
||||
box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
|
||||
//border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background-color: @background-3;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
@@ -20,6 +19,7 @@ header {
|
||||
select {
|
||||
-webkit-app-region: no-drag;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -27,6 +27,13 @@ header {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
|
||||
|
||||
@@ -2,6 +2,43 @@
|
||||
animation: fadein 0.15s;
|
||||
}
|
||||
|
||||
.mosaic-window-toolbar {
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 5px 0 5px;
|
||||
|
||||
.mosaic-controls {
|
||||
transform: scale(0.75);
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update if list of Editor ID changes
|
||||
@editor-ids: main, renderer, html, preload;
|
||||
|
||||
// takes each editor ID and increase its z-index if the parent Mosiac root
|
||||
// has focused__id class for that specific id.
|
||||
each(@editor-ids, {
|
||||
.focused__@{value}.mosaic .mosaic-window.@{value} {
|
||||
z-index: 2;
|
||||
}
|
||||
});
|
||||
|
||||
.mosaic-window {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import "../variables.less";
|
||||
|
||||
.fiddle {
|
||||
.mosaic-window-toolbar.draggable {
|
||||
.mosaic-window-toolbar {
|
||||
background: var(--background-1);
|
||||
box-shadow: inset 0 0 100px 100px rgba(53, 53, 53, 0.1);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
@@ -34,9 +34,9 @@
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
font-size: 12px;
|
||||
overflow: visible;
|
||||
z-index: 4;
|
||||
background-color: @background-4;
|
||||
}
|
||||
|
||||
.mosaic {
|
||||
|
||||
@@ -7,21 +7,15 @@
|
||||
color: var(--text-color-1);
|
||||
background: var(--background-2);
|
||||
box-shadow: inset 0 0 100px 100px rgba(0, 0, 0, 0.15);
|
||||
border-top: @border;
|
||||
top: 10vh;
|
||||
padding: 0;
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
height: 0;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
transition: height 0.2s;
|
||||
transition: padding 0.2s;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&.showing {
|
||||
border-top: @border;
|
||||
padding-top: 10px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
white-space: pre;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.show-me-panel {
|
||||
padding: 10px;
|
||||
overflow: scroll;
|
||||
|
||||
& > *:first-child {
|
||||
margin-top: 0;
|
||||
};
|
||||
|
||||
.bp3-callout {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.show-me-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
max-height: 400px;
|
||||
}
|
||||
@@ -6,3 +6,7 @@
|
||||
width: 100vw;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.tour-portal {
|
||||
z-index: 1000;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.bp3-fill {
|
||||
.version-chooser {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,6 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,6 @@
|
||||
|
||||
// Variables, also imported in each file
|
||||
@import "variables.less";
|
||||
|
||||
@import "main.less";
|
||||
@import "container.less";
|
||||
@import "mosaic-vendor.less";
|
||||
@@ -17,4 +16,5 @@
|
||||
@import "components/chrome-mac.less";
|
||||
@import "components/editors.less";
|
||||
@import "components/tour.less";
|
||||
@import "components/commands-version-chooser.less";
|
||||
@import "components/show-me.less";
|
||||
@import "components/version-select.less";
|
||||
|
||||
@@ -15,6 +15,11 @@ export function getRunItems(): Array<MenuItemConstructorOptions> {
|
||||
label: 'Run Fiddle',
|
||||
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
|
||||
},
|
||||
{
|
||||
id: 'clear_console',
|
||||
label: 'Clear Console',
|
||||
click: () => ipcMainManager.send(IpcEvents.CLEAR_CONSOLE)
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
}
|
||||
@@ -136,17 +141,17 @@ export function createContextMenu(browserWindow: BrowserWindow) {
|
||||
{
|
||||
id: 'cut',
|
||||
label: 'Cut',
|
||||
role: editFlags.canCut ? 'cut' : '',
|
||||
role: 'cut',
|
||||
enabled: editFlags.canCut
|
||||
}, {
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
role: editFlags.canCopy ? 'copy' : '',
|
||||
role: 'copy',
|
||||
enabled: editFlags.canCopy
|
||||
}, {
|
||||
id: 'paste',
|
||||
label: 'Paste',
|
||||
role: editFlags.canPaste ? 'paste' : '',
|
||||
role: 'paste',
|
||||
enabled: editFlags.canPaste
|
||||
}, {
|
||||
type: 'separator'
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
|
||||
/**
|
||||
* Installs developer tools if we're in dev mode.
|
||||
*
|
||||
* @export
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function setupDevTools(): Promise<void> {
|
||||
if (!isDevMode()) return;
|
||||
|
||||
const {
|
||||
default: installExtension,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
REACT_PERF
|
||||
} = require('electron-devtools-installer');
|
||||
|
||||
try {
|
||||
const react = await installExtension(REACT_DEVELOPER_TOOLS);
|
||||
console.log(`installDevTools: Installed ${react}`);
|
||||
|
||||
const perf = await installExtension(REACT_PERF);
|
||||
console.log(`installDevTools: Installed ${perf}`);
|
||||
} catch (error) {
|
||||
console.warn(`installDevTools: Error occured:`, error);
|
||||
}
|
||||
}
|
||||
+35
-1
@@ -1,4 +1,4 @@
|
||||
import { dialog } from 'electron';
|
||||
import { dialog, IpcMainEvent } from 'electron';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { ipcMainManager } from './ipc';
|
||||
import { getOrCreateMainWindow } from './windows';
|
||||
@@ -12,6 +12,15 @@ export function setupDialogs() {
|
||||
ipcMainManager.on(IpcEvents.SHOW_WARNING_DIALOG, (_event, args) => {
|
||||
showWarningDialog(args);
|
||||
});
|
||||
|
||||
ipcMainManager.on(IpcEvents.SHOW_CONFIRMATION_DIALOG, (_event, args) => {
|
||||
showConfirmationDialog(args);
|
||||
});
|
||||
|
||||
ipcMainManager.on(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, async (event) => {
|
||||
await showOpenDialog(event);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,3 +34,28 @@ function showWarningDialog(args: Electron.MessageBoxOptions) {
|
||||
...args
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog
|
||||
*
|
||||
* @param {Electron.MessageBoxOptions} args
|
||||
*/
|
||||
function showConfirmationDialog(args: Electron.MessageBoxOptions) {
|
||||
dialog.showMessageBox(getOrCreateMainWindow(), {
|
||||
type: 'warning',
|
||||
...args
|
||||
});
|
||||
}
|
||||
|
||||
async function showOpenDialog(event: IpcMainEvent) {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
title: 'Open Folder',
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
|
||||
if (!filePaths || filePaths.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.reply(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, [filePaths[0]]);
|
||||
}
|
||||
|
||||
+25
-23
@@ -2,56 +2,58 @@ import { dialog } from 'electron';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../constants';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../shared-constants';
|
||||
import { ipcMainManager } from './ipc';
|
||||
|
||||
/**
|
||||
* Ensures that we're listening to file events
|
||||
*/
|
||||
export function setupFileListeners() {
|
||||
ipcMainManager.on(IpcEvents.FS_SAVE_FIDDLE_DIALOG, () => showSaveDialog());
|
||||
ipcMainManager.on(IpcEvents.FS_SAVE_FIDDLE_DIALOG, () => {
|
||||
showSaveDialog();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the "Open Fiddle" dialog and forwards
|
||||
* the path to the renderer
|
||||
*/
|
||||
export function showOpenDialog() {
|
||||
dialog.showOpenDialog({
|
||||
export async function showOpenDialog() {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
title: 'Open Fiddle',
|
||||
properties: ['openDirectory']
|
||||
}, (filePaths) => {
|
||||
if (!filePaths || filePaths.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
|
||||
});
|
||||
|
||||
if (!filePaths || filePaths.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the "Save Fiddle" dialog and forwards
|
||||
* the path to the renderer
|
||||
*/
|
||||
export function showSaveDialog(event?: IpcEvents, as?: string) {
|
||||
export async function showSaveDialog(event?: IpcEvents, as?: string) {
|
||||
// We want to save to a folder, so we'll use an open dialog here
|
||||
dialog.showOpenDialog({
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
buttonLabel: 'Save here',
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
title: `Save Fiddle${as ? ` as ${as}` : ''}`
|
||||
}, async (filePaths) => {
|
||||
if (!filePaths || filePaths.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Asked to save to ${filePaths[0]}`);
|
||||
|
||||
// Let's confirm real quick if we want this
|
||||
if (await ensureSaveTargetEmpty(filePaths[0])) {
|
||||
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
|
||||
}
|
||||
});
|
||||
|
||||
if (!filePaths || filePaths.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Asked to save to ${filePaths[0]}`);
|
||||
|
||||
// Let's confirm real quick if we want this
|
||||
if (await ensureSaveTargetEmpty(filePaths[0])) {
|
||||
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { app, dialog } from 'electron';
|
||||
|
||||
import { isFirstRun } from '../utils/check-first-run';
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
|
||||
/**
|
||||
* Is this the first run of Fiddle? If so, perform
|
||||
* tasks that we only want to do in this case.
|
||||
*/
|
||||
export async function onFirstRunMaybe() {
|
||||
if (isFirstRun()) {
|
||||
await promptMoveToApplicationsFolder();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user if the app should be moved to the
|
||||
* applications folder.
|
||||
*/
|
||||
async function promptMoveToApplicationsFolder(): Promise<void> {
|
||||
if (process.platform !== 'darwin') return;
|
||||
if (isDevMode() || app.isInApplicationsFolder()) return;
|
||||
|
||||
const { response } = await dialog.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ['Move to Applications Folder', 'Do Not Move'],
|
||||
defaultId: 0,
|
||||
message: 'Move to Applications Folder?',
|
||||
});
|
||||
|
||||
if (response === 0) {
|
||||
app.moveToApplicationsFolder();
|
||||
}
|
||||
}
|
||||
+22
-1
@@ -1,6 +1,7 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { EventEmitter } from 'events';
|
||||
import { IpcEvents, ipcMainEvents } from '../ipc-events';
|
||||
|
||||
import { IpcEvents, ipcMainEvents, WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
|
||||
import { getOrCreateMainWindow } from './windows';
|
||||
|
||||
/**
|
||||
@@ -12,12 +13,27 @@ import { getOrCreateMainWindow } from './windows';
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
export class IpcMainManager extends EventEmitter {
|
||||
public readyWebContents = new WeakSet<Electron.WebContents>();
|
||||
private messageQueue = new WeakMap<Electron.WebContents, Array<[IpcEvents, Array<any> | undefined]>>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
ipcMainEvents.forEach((name) => {
|
||||
ipcMain.removeAllListeners(name);
|
||||
ipcMain.on(name, (...args: Array<any>) => this.emit(name, ...args));
|
||||
});
|
||||
|
||||
ipcMain.on(WEBCONTENTS_READY_FOR_IPC_SIGNAL, (event: Electron.IpcMainEvent) => {
|
||||
this.readyWebContents.add(event.sender);
|
||||
|
||||
const queue = this.messageQueue.get(event.sender);
|
||||
this.messageQueue.delete(event.sender);
|
||||
if (!queue) return;
|
||||
for (const item of queue) {
|
||||
this.send(item[0], item[1], event.sender);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +47,11 @@ export class IpcMainManager extends EventEmitter {
|
||||
public send(channel: IpcEvents, args?: Array<any>, target?: Electron.WebContents) {
|
||||
const _target = target || getOrCreateMainWindow().webContents;
|
||||
const _args = args || [];
|
||||
if (!this.readyWebContents.has(_target)) {
|
||||
const existing = this.messageQueue.get(_target) || [];
|
||||
this.messageQueue.set(_target, [...existing, [channel, args]]);
|
||||
return;
|
||||
}
|
||||
|
||||
_target.send(channel, ..._args);
|
||||
}
|
||||
|
||||
+69
-20
@@ -1,28 +1,30 @@
|
||||
import { initSentry } from '../sentry';
|
||||
initSentry();
|
||||
|
||||
import { app } from 'electron';
|
||||
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
import { setupAboutPanel } from '../utils/set-about-panel';
|
||||
import { setupDevTools } from './devtools';
|
||||
import { setupDialogs } from './dialogs';
|
||||
import { onFirstRunMaybe } from './first-run';
|
||||
import { ipcMainManager } from './ipc';
|
||||
import { listenForProtocolHandler, setupProtocolHandler } from './protocol';
|
||||
import { shouldQuit } from './squirrel';
|
||||
import { setupUpdates } from './update';
|
||||
import { getOrCreateMainWindow } from './windows';
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (require('electron-squirrel-startup')) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
app.setName('Electron Fiddle');
|
||||
|
||||
listenForProtocolHandler();
|
||||
|
||||
app.on('ready', async () => {
|
||||
// If we're packaged, we want to run
|
||||
// React in production mode.
|
||||
if (!isDevMode()) {
|
||||
process.env.NODE_ENV = 'production';
|
||||
}
|
||||
/**
|
||||
* Handle the app's "ready" event. This is essentially
|
||||
* the method that takes care of booting the application.
|
||||
*/
|
||||
export async function onReady() {
|
||||
await onFirstRunMaybe();
|
||||
if (!isDevMode()) process.env.NODE_ENV = 'production';
|
||||
|
||||
getOrCreateMainWindow();
|
||||
setupAboutPanel();
|
||||
|
||||
const { setupMenu } = await import('./menu');
|
||||
const { setupFileListeners } = await import('./files');
|
||||
@@ -32,15 +34,62 @@ app.on('ready', async () => {
|
||||
setupFileListeners();
|
||||
setupUpdates();
|
||||
setupDialogs();
|
||||
});
|
||||
setupDevTools();
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
/**
|
||||
* Handle the "before-quit" event
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function onBeforeQuit() {
|
||||
ipcMainManager.send(IpcEvents.BEFORE_QUIT);
|
||||
ipcMainManager.on(IpcEvents.CONFIRM_QUIT, quitAppIfConfirmed);
|
||||
}
|
||||
|
||||
export function quitAppIfConfirmed(_: Electron.IpcMainEvent, quitConfirmed: boolean) {
|
||||
if (quitConfirmed) {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All windows have been closed, quit on anything but
|
||||
* macOS.
|
||||
*/
|
||||
export function onWindowsAllClosed() {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.on('activate', getOrCreateMainWindow);
|
||||
/**
|
||||
* The main method - and the first function to run
|
||||
* when Fiddle is launched.
|
||||
*
|
||||
* Exported for testing purposes.
|
||||
*/
|
||||
export function main() {
|
||||
// Handle creating/removing shortcuts on Windows when
|
||||
// installing/uninstalling.
|
||||
if (shouldQuit()) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the app's name
|
||||
app.name = 'Electron Fiddle';
|
||||
|
||||
// Ensure that there's only ever one Fiddle running
|
||||
listenForProtocolHandler();
|
||||
|
||||
// Launch
|
||||
app.on('ready', onReady);
|
||||
app.on('before-quit', onBeforeQuit);
|
||||
app.on('window-all-closed', onWindowsAllClosed);
|
||||
app.on('activate', getOrCreateMainWindow);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
+20
-14
@@ -105,9 +105,7 @@ function getQuitItems(): Array<MenuItemConstructorOptions> {
|
||||
{
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Exit',
|
||||
accelerator: 'Ctrl+Q,',
|
||||
click: app.quit
|
||||
role: 'quit'
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -121,6 +119,7 @@ function getTasksMenu(): MenuItemConstructorOptions {
|
||||
const tasksMenu: Array<MenuItemConstructorOptions> = [
|
||||
{
|
||||
label: 'Run Fiddle...',
|
||||
accelerator: 'F5',
|
||||
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
|
||||
},
|
||||
{
|
||||
@@ -143,7 +142,7 @@ function getShowMeMenuItem(key: string, item: string | Templates): MenuItemConst
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
label: key,
|
||||
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [ key ])
|
||||
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [key])
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,10 +158,10 @@ function getShowMeMenu(): MenuItemConstructorOptions {
|
||||
const showMeMenu: Array<MenuItemConstructorOptions> = Object.keys(SHOW_ME_TEMPLATES)
|
||||
.map((key) => getShowMeMenuItem(key, SHOW_ME_TEMPLATES[key]));
|
||||
|
||||
return {
|
||||
label: 'Show Me',
|
||||
submenu: showMeMenu
|
||||
};
|
||||
return {
|
||||
label: 'Show Me',
|
||||
submenu: showMeMenu
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,7 +204,7 @@ function getFileMenu(): MenuItemConstructorOptions {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Save to Gist',
|
||||
label: 'Publish to Gist',
|
||||
click: () => ipcMainManager.send(IpcEvents.FS_SAVE_FIDDLE_GIST),
|
||||
},
|
||||
{
|
||||
@@ -238,7 +237,7 @@ export function setupMenu() {
|
||||
// Append the "Settings" item
|
||||
if (
|
||||
process.platform === 'darwin'
|
||||
&& label === app.getName()
|
||||
&& label === app.name
|
||||
&& isSubmenu(item.submenu)
|
||||
) {
|
||||
item.submenu.splice(2, 0, ...getPreferencesItems());
|
||||
@@ -246,15 +245,22 @@ export function setupMenu() {
|
||||
|
||||
// Tweak "View" menu
|
||||
if (label === 'View' && isSubmenu(item.submenu)) {
|
||||
item.submenu = item.submenu.filter((subItem) => subItem.label !== 'Toggle Developer Tools'); // Remove "Toggle Developer Tools"
|
||||
item.submenu.push({ type: 'separator' }, { role: 'resetzoom' }, { role: 'zoomin' }, { role: 'zoomout' }); // Add zooming actions
|
||||
// remove "Reload" (has weird behaviour) and "Toggle Developer Tools"
|
||||
item.submenu = item.submenu
|
||||
.filter((subItem) => subItem.label !== 'Toggle Developer Tools' && subItem.label !== 'Reload');
|
||||
item.submenu.push({ type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }); // Add zooming actions
|
||||
item.submenu.push({ type: 'separator' }, {
|
||||
label: 'Toggle Soft Wrap',
|
||||
click: () => ipcMainManager.send(IpcEvents.TOGGLE_SOFT_WRAP),
|
||||
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['wordWrap']),
|
||||
});
|
||||
item.submenu.push({ type: 'separator' }, {
|
||||
label: 'Toggle Mini Map',
|
||||
click: () => ipcMainManager.send(IpcEvents.TOGGLE_MINI_MAP),
|
||||
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['minimap.enabled']),
|
||||
});
|
||||
item.submenu.push({ type: 'separator' }, {
|
||||
label: 'Toggle Bisect Helper',
|
||||
click: () => ipcMainManager.send(IpcEvents.BISECT_COMMANDS_TOGGLE),
|
||||
accelerator: 'CommandorControl+Shift+B',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+53
-6
@@ -4,6 +4,7 @@ import * as path from 'path';
|
||||
import * as nodeUrl from 'url';
|
||||
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
import { ipcMainManager } from './ipc';
|
||||
|
||||
const PROTOCOL = 'electron-fiddle';
|
||||
@@ -14,32 +15,78 @@ const handlePotentialProtocolLaunch = (url: string) => {
|
||||
app.once('ready', () => handlePotentialProtocolLaunch(url));
|
||||
return;
|
||||
}
|
||||
const parsed = nodeUrl.parse(url.replace(/\/$/, ''));
|
||||
if (!parsed.pathname) return;
|
||||
const pathParts = parsed.pathname.split('/');
|
||||
if (pathParts.length <= 0 || pathParts.length > 2) return;
|
||||
|
||||
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [parsed.pathname]);
|
||||
const parsed = nodeUrl.parse(url.replace(/\/$/, ''));
|
||||
if (!parsed.pathname || !parsed.hostname) return;
|
||||
|
||||
const pathParts = parsed.pathname.split('/').slice(1);
|
||||
|
||||
switch (parsed.hostname) {
|
||||
// electron-fiddle://gist/blub
|
||||
case 'gist':
|
||||
if (pathParts.length === 1) {
|
||||
// We only have a gist ID
|
||||
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [{
|
||||
id: pathParts[0],
|
||||
}]);
|
||||
} else if (pathParts.length === 2) {
|
||||
// We have a gist owner and gist ID, we can ignore the owner
|
||||
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [{
|
||||
id: pathParts[1],
|
||||
}]);
|
||||
} else {
|
||||
// This is a super invalid gist launch
|
||||
return;
|
||||
}
|
||||
break;
|
||||
// electron-fiddle://electron/{ref}/{path}
|
||||
case 'electron':
|
||||
if (pathParts.length > 1) {
|
||||
// First part of the commit HASH / ref / branch
|
||||
// Rest is the path to the example
|
||||
ipcMainManager.send(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, [{
|
||||
ref: pathParts[0],
|
||||
path: pathParts.slice(1).join('/'),
|
||||
}]);
|
||||
} else {
|
||||
// This is an invalid electron launch
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const scanArgv = (argv: Array<string>) => {
|
||||
const protocolArg = argv.find((arg) => arg.startsWith(`${PROTOCOL}://`));
|
||||
if (protocolArg) {
|
||||
console.info('Found protocol arg in argv:', protocolArg);
|
||||
handlePotentialProtocolLaunch(protocolArg);
|
||||
}
|
||||
};
|
||||
|
||||
export const scanNpmArgv = (argv: string) => {
|
||||
const parsedArgv = JSON.parse(argv);
|
||||
const { original: args } = parsedArgv;
|
||||
scanArgv(args);
|
||||
};
|
||||
|
||||
export const listenForProtocolHandler = () => {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) app.quit();
|
||||
|
||||
app.removeAllListeners('open-url');
|
||||
app.on('open-url', (_, url) => {
|
||||
if (url.startsWith(`${PROTOCOL}://`)) {
|
||||
handlePotentialProtocolLaunch(url);
|
||||
}
|
||||
});
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// pass protocol URL via npm start args in dev mode
|
||||
if (isDevMode() && process.env.npm_config_argv) {
|
||||
scanNpmArgv(process.env.npm_config_argv);
|
||||
} else if (process.platform === 'win32') {
|
||||
scanArgv(process.argv);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function shouldQuit() {
|
||||
return require('electron-squirrel-startup');
|
||||
}
|
||||
+3
-1
@@ -5,7 +5,9 @@ export function setupUpdates() {
|
||||
// We delay this work by 10s to ensure that the
|
||||
// app doesn't have to worry about updating during launch
|
||||
setTimeout(() => {
|
||||
require('update-electron-app')({
|
||||
const updateApp = require('update-electron-app');
|
||||
|
||||
updateApp({
|
||||
repo: 'electron/fiddle',
|
||||
updateInterval: '1 hour',
|
||||
});
|
||||
|
||||
+21
-6
@@ -1,9 +1,11 @@
|
||||
import { BrowserWindow, shell } from 'electron';
|
||||
import { app, BrowserWindow, shell } from 'electron';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { createContextMenu } from './context-menu';
|
||||
import { ipcMainManager } from './ipc';
|
||||
|
||||
// Keep a global reference of the window objects, if we don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
export let browserWindows: Array<BrowserWindow> = [];
|
||||
export let browserWindows: Array<BrowserWindow | null> = [];
|
||||
|
||||
/**
|
||||
* Gets default options for the main window
|
||||
@@ -20,7 +22,8 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
|
||||
acceptFirstMouse: true,
|
||||
backgroundColor: '#1d2427',
|
||||
webPreferences: {
|
||||
webviewTag: false
|
||||
webviewTag: false,
|
||||
nodeIntegration: true
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -34,13 +37,14 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
|
||||
*/
|
||||
export function createMainWindow(): Electron.BrowserWindow {
|
||||
console.log(`Creating main window`);
|
||||
const browserWindow = new BrowserWindow(getMainWindowOptions());
|
||||
let browserWindow: BrowserWindow | null;
|
||||
browserWindow = new BrowserWindow(getMainWindowOptions());
|
||||
browserWindow.loadFile('./dist/static/index.html');
|
||||
|
||||
browserWindow.webContents.once('dom-ready', () => {
|
||||
browserWindow.show();
|
||||
|
||||
if (browserWindow) {
|
||||
browserWindow.show();
|
||||
|
||||
createContextMenu(browserWindow);
|
||||
}
|
||||
});
|
||||
@@ -48,6 +52,8 @@ export function createMainWindow(): Electron.BrowserWindow {
|
||||
browserWindow.on('closed', () => {
|
||||
browserWindows = browserWindows
|
||||
.filter((bw) => browserWindow !== bw);
|
||||
|
||||
browserWindow = null;
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('new-window', (event, url) => {
|
||||
@@ -60,6 +66,15 @@ export function createMainWindow(): Electron.BrowserWindow {
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
ipcMainManager.on(IpcEvents.SHOW_INACTIVE, () => {
|
||||
if (browserWindow) {
|
||||
browserWindow.showInactive();
|
||||
}
|
||||
});
|
||||
|
||||
const appData = app.getPath('appData');
|
||||
ipcMainManager.send(IpcEvents.SET_APPDATA_DIR, [appData], browserWindow.webContents);
|
||||
|
||||
browserWindows.push(browserWindow);
|
||||
|
||||
return browserWindow;
|
||||
|
||||
+126
-76
@@ -1,12 +1,30 @@
|
||||
import { initSentry } from '../sentry';
|
||||
initSentry();
|
||||
|
||||
import { when } from 'mobx';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
|
||||
import { EditorValues } from '../interfaces';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import {
|
||||
ALL_EDITORS,
|
||||
EditorId,
|
||||
EditorValues,
|
||||
GenericDialogType,
|
||||
SetFiddleOptions
|
||||
} from '../interfaces';
|
||||
import { WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
|
||||
import { updateEditorLayout } from '../utils/editor-layout';
|
||||
import { getEditorValue } from '../utils/editor-value';
|
||||
import { getPackageJson, PackageJsonOptions } from '../utils/get-package';
|
||||
import { getTitle } from '../utils/get-title';
|
||||
import { isEditorBackup } from '../utils/type-checks';
|
||||
import { EMPTY_EDITOR_CONTENT } from './constants';
|
||||
import { FileManager } from './file-manager';
|
||||
import { RemoteLoader } from './remote-loader';
|
||||
import { Runner } from './runner';
|
||||
import { appState } from './state';
|
||||
import { getTheme } from './themes';
|
||||
import { TouchBarManager } from './touch-bar-manager';
|
||||
|
||||
/**
|
||||
* The top-level class controlling the whole app. This is *not* a React component,
|
||||
@@ -19,75 +37,122 @@ export class App {
|
||||
public monaco: typeof MonacoType | null = null;
|
||||
public state = appState;
|
||||
public fileManager = new FileManager(appState);
|
||||
public remoteLoader = new RemoteLoader(appState);
|
||||
public runner = new Runner(appState);
|
||||
public touchBarManager: TouchBarManager | undefined;
|
||||
|
||||
constructor() {
|
||||
this.getValues = this.getValues.bind(this);
|
||||
this.setValues = this.setValues.bind(this);
|
||||
this.getEditorValues = this.getEditorValues.bind(this);
|
||||
this.setEditorValues = this.setEditorValues.bind(this);
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
this.touchBarManager = new TouchBarManager(appState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the values on all three editors.
|
||||
*
|
||||
* @param {EditorValues} values
|
||||
* @param {warn} warn - Should we warn before overwriting unsaved data?
|
||||
*/
|
||||
public async setValues(values: Partial<EditorValues>, warn: boolean = true): Promise<boolean> {
|
||||
const { ElectronFiddle: fiddle } = window;
|
||||
public async replaceFiddle(
|
||||
editorValues: Partial<EditorValues>,
|
||||
{ filePath, gistId, templateName }: Partial<SetFiddleOptions>
|
||||
) {
|
||||
// if unsaved, prompt user to make sure they're okay with overwriting and changing directory
|
||||
if (this.state.isUnsaved) {
|
||||
this.state.setGenericDialogOptions({
|
||||
type: GenericDialogType.warning,
|
||||
label: `Opening this Fiddle will replace your unsaved changes. Do you want to proceed?`,
|
||||
ok: 'Yes'
|
||||
});
|
||||
this.state.isGenericDialogShowing = true;
|
||||
await when(() => !this.state.isGenericDialogShowing);
|
||||
|
||||
if (!fiddle) {
|
||||
throw new Error('Fiddle not ready');
|
||||
}
|
||||
|
||||
if (appState.isUnsaved && warn) {
|
||||
this.state.isWarningDialogShowing = true;
|
||||
await when(() => !this.state.isWarningDialogShowing);
|
||||
|
||||
if (!this.state.warningDialogLastResult) {
|
||||
if (!this.state.genericDialogLastResult) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const { main, html, renderer } = fiddle.editors;
|
||||
// display all editors that have content
|
||||
const visibleEditors = [];
|
||||
for (const id of Object.keys(editorValues)) {
|
||||
const content = editorValues[id];
|
||||
|
||||
if (html && html.setValue && values.html) {
|
||||
html.setValue(values.html);
|
||||
// if the gist content matches the empty file output, don't show it
|
||||
if (!Object.values(EMPTY_EDITOR_CONTENT).includes(content)) {
|
||||
visibleEditors.push(id as EditorId);
|
||||
}
|
||||
}
|
||||
|
||||
if (main && main.setValue && values.main) {
|
||||
main.setValue(values.main);
|
||||
}
|
||||
|
||||
if (renderer && renderer.setValue && values.renderer) {
|
||||
renderer.setValue(values.renderer);
|
||||
}
|
||||
document.title = getTitle(this.state);
|
||||
this.state.gistId = gistId || '';
|
||||
this.state.localPath = filePath;
|
||||
this.state.templateName = templateName;
|
||||
|
||||
appState.isUnsaved = false;
|
||||
this.setupUnsavedOnChangeListener();
|
||||
// once loaded, we have a "saved" state
|
||||
await this.state.setVisibleMosaics(visibleEditors);
|
||||
await this.setEditorValues(editorValues);
|
||||
this.state.isUnsaved = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values on all three editors.
|
||||
* Sets the contents of all editor panes.
|
||||
*
|
||||
* @returns {EditorValues}
|
||||
* @param {EditorValues} values
|
||||
*/
|
||||
public async getValues(options?: PackageJsonOptions): Promise<EditorValues> {
|
||||
public async setEditorValues(values: Partial<EditorValues>): Promise<void> {
|
||||
const { ElectronFiddle: fiddle } = window;
|
||||
|
||||
if (!fiddle) {
|
||||
throw new Error('Fiddle not ready');
|
||||
}
|
||||
|
||||
for (const name of ALL_EDITORS) {
|
||||
const editor = fiddle.editors[name];
|
||||
const backup = this.state.closedPanels[name];
|
||||
|
||||
if (typeof values[name] !== 'undefined') {
|
||||
if (isEditorBackup(backup)) {
|
||||
// The editor does not exist, attempt to set it on the backup.
|
||||
// If there's a model, we'll do it on the model. Else, we'll
|
||||
// set the value.
|
||||
|
||||
if (backup.model) {
|
||||
backup.model.setValue(values[name]!);
|
||||
} else {
|
||||
backup.value = values[name]!;
|
||||
}
|
||||
} else if (editor && editor.setValue) {
|
||||
// The editor exists, set the value directly
|
||||
editor.setValue(values[name]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contents of all editor panes.
|
||||
*
|
||||
* @returns {EditorValues}
|
||||
*/
|
||||
public async getEditorValues(
|
||||
options?: PackageJsonOptions
|
||||
): Promise<EditorValues> {
|
||||
const { ElectronFiddle: fiddle } = window;
|
||||
|
||||
if (!fiddle) {
|
||||
throw new Error('Fiddle not ready');
|
||||
}
|
||||
|
||||
const { main, html, renderer } = fiddle.editors;
|
||||
const values: EditorValues = {
|
||||
html: html && html.getValue() ? html.getValue() : '',
|
||||
main: main && main.getValue() ? main.getValue() : '',
|
||||
renderer: renderer && renderer.getValue() ? renderer.getValue() : '',
|
||||
css: getEditorValue(EditorId.css),
|
||||
html: getEditorValue(EditorId.html),
|
||||
main: getEditorValue(EditorId.main),
|
||||
preload: getEditorValue(EditorId.preload),
|
||||
renderer: getEditorValue(EditorId.renderer)
|
||||
};
|
||||
|
||||
if (options && options.include !== false) {
|
||||
values.package = await getPackageJson(appState, values, options);
|
||||
if (options && options.include !== false) {
|
||||
values.package = await getPackageJson(this.state, values, options);
|
||||
}
|
||||
|
||||
return values;
|
||||
@@ -97,48 +162,33 @@ export class App {
|
||||
* Initial setup call, loading Monaco and kicking off the React
|
||||
* render process.
|
||||
*/
|
||||
public async setup(): Promise<void> {
|
||||
public async setup(): Promise<void | Element | React.Component> {
|
||||
this.setupTheme();
|
||||
|
||||
const React = await import('react');
|
||||
const { render } = await import('react-dom');
|
||||
const { Header } = await import('./components/header');
|
||||
const { Dialogs } = await import('./components/dialogs');
|
||||
const { Editors } = await import('./components/editors');
|
||||
const { OutputEditorsWrapper } = await import(
|
||||
'./components/output-editors-wrapper'
|
||||
);
|
||||
const { Header } = await import('./components/header');
|
||||
|
||||
const className = `${process.platform} container`;
|
||||
const app = (
|
||||
<div className={className}>
|
||||
<Header appState={appState} />
|
||||
<Dialogs appState={appState} />
|
||||
<Editors appState={appState} />
|
||||
<Dialogs appState={this.state} />
|
||||
<Header appState={this.state} />
|
||||
<OutputEditorsWrapper appState={this.state} />
|
||||
</div>
|
||||
);
|
||||
|
||||
render(app, document.getElementById('app'));
|
||||
const rendered = render(app, document.getElementById('app'));
|
||||
|
||||
this.setupResizeListener();
|
||||
|
||||
// Todo: A timer here is terrible. Let's fix this
|
||||
// and ensure we actually do it once Editors have mounted.
|
||||
setTimeout(() => {
|
||||
this.setupUnsavedOnChangeListener();
|
||||
}, 1500);
|
||||
}
|
||||
ipcRenderer.send(WEBCONTENTS_READY_FOR_IPC_SIGNAL);
|
||||
|
||||
/**
|
||||
* If the editor is changed for the first time, we'll
|
||||
* set `isUnsaved` to true. That way, the app can warn you
|
||||
* if you're about to throw things away.
|
||||
*/
|
||||
public setupUnsavedOnChangeListener() {
|
||||
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
|
||||
const editor = window.ElectronFiddle.editors[key];
|
||||
const disposable = editor.onDidChangeModelContent(() => {
|
||||
appState.isUnsaved = true;
|
||||
disposable.dispose();
|
||||
});
|
||||
});
|
||||
return rendered;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,14 +197,16 @@ export class App {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async setupTheme(): Promise<void> {
|
||||
const tag: HTMLStyleElement | null = document.querySelector('style#fiddle-theme');
|
||||
const theme = await getTheme(appState.theme);
|
||||
const tag: HTMLStyleElement | null = document.querySelector(
|
||||
'style#fiddle-theme'
|
||||
);
|
||||
const theme = await getTheme(this.state.theme);
|
||||
|
||||
if (tag && theme.css) {
|
||||
tag.innerHTML = theme.css;
|
||||
}
|
||||
|
||||
if (theme.isDark || (theme.name || '').includes('dark')) {
|
||||
if (theme.isDark || theme.name.includes('dark')) {
|
||||
document.body.classList.add('bp3-dark');
|
||||
} else {
|
||||
document.body.classList.remove('bp3-dark');
|
||||
@@ -170,10 +222,8 @@ export class App {
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
if (!process.env.TEST && !process.env.JEST_WORKER_ID) {
|
||||
window.ElectronFiddle.contentChangeListeners = [];
|
||||
window.ElectronFiddle.app = new App();
|
||||
window.ElectronFiddle.app.setup()
|
||||
.catch((error) => console.error(error));
|
||||
}
|
||||
window.ElectronFiddle = window.ElectronFiddle || {};
|
||||
window.ElectronFiddle.contentChangeListeners =
|
||||
window.ElectronFiddle.contentChangeListeners || [];
|
||||
window.ElectronFiddle.app = window.ElectronFiddle.app || new App();
|
||||
window.ElectronFiddle.app.setup();
|
||||
|
||||
+237
-173
@@ -1,195 +1,259 @@
|
||||
import * as fsType from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { USER_DATA_PATH } from '../constants';
|
||||
import { VersionState } from '../interfaces';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { normalizeVersion } from '../utils/normalize-version';
|
||||
import { USER_DATA_PATH } from './constants';
|
||||
import { removeTypeDefsForVersion } from './fetch-types';
|
||||
import { AppState } from './state';
|
||||
|
||||
/**
|
||||
* The binary manager takes care of downloading Electron versions
|
||||
* General setup, called with a version. Is called during construction
|
||||
* to ensure that we always have or download at least one version.
|
||||
*
|
||||
* @export
|
||||
* @class BinaryManager
|
||||
* @param {string} iVersion
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export class BinaryManager {
|
||||
public state: Record<string, 'ready' | 'downloading'> = {};
|
||||
export async function setupBinary(appState: AppState, iVersion: string): Promise<void> {
|
||||
const version = normalizeVersion(iVersion);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
|
||||
/**
|
||||
* Remove a version from disk. Does not update state. We'll try up to
|
||||
* four times before giving up if an error occurs.
|
||||
*
|
||||
* @param {string} iVersion
|
||||
* @param {number} iteration
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async remove(iVersion: string, i: number = 0): Promise<void> {
|
||||
const version = normalizeVersion(iVersion);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
await fs.mkdirp(getDownloadPath(version));
|
||||
|
||||
const { state } = appState.versions[version];
|
||||
if (state === VersionState.downloading || state === VersionState.unzipping) {
|
||||
console.log(`Binary: Electron ${version} already downloading.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await getIsDownloaded(version)) {
|
||||
console.log(`Binary: Electron ${version} already downloaded.`);
|
||||
appState.versions[version].state = VersionState.ready;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Binary: Electron ${version} not present, downloading`);
|
||||
appState.versions[version].state = VersionState.downloading;
|
||||
|
||||
const zipPath = await download(appState, version);
|
||||
const extractPath = getDownloadPath(version);
|
||||
console.log(`Binary: Electron ${version} downloaded, now unpacking to ${extractPath}`);
|
||||
|
||||
try {
|
||||
appState.versions[version].state = VersionState.unzipping;
|
||||
|
||||
// Ensure the target path is empty
|
||||
await fs.emptyDir(extractPath);
|
||||
|
||||
const electronFiles = await unzip(zipPath, extractPath);
|
||||
console.log(`Unzipped ${version}`, electronFiles);
|
||||
} catch (error) {
|
||||
console.warn(`Failure while unzipping ${version}`, error);
|
||||
appState.versions[version].state = VersionState.unknown;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
appState.versions[version].state = VersionState.ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a version from disk. Does not update state. We'll try up to
|
||||
* four times before giving up if an error occurs.
|
||||
*
|
||||
* @param {string} iVersion
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function removeBinary(iVersion: string) {
|
||||
const version = normalizeVersion(iVersion);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
let isDeleted = false;
|
||||
|
||||
// utility to re-run removal functions upon failure
|
||||
// due to windows filesystem lockfile jank
|
||||
const rerunner = async (func: () => Promise<void>, counter: number = 1) => {
|
||||
try {
|
||||
if (await this.getIsDownloaded(version)) {
|
||||
// This is necessary since we're messing with .asar files inside
|
||||
// the Electron binaries. Electron, powering Fiddle, will try to
|
||||
// "correct" our calls, but we don't want that right here.
|
||||
process.noAsar = true;
|
||||
|
||||
await fs.remove(this.getDownloadPath(version));
|
||||
|
||||
process.noAsar = false;
|
||||
}
|
||||
await func();
|
||||
} catch (error) {
|
||||
console.warn(`Binary Manager: Tried to remove ${version}, but failed`, error);
|
||||
|
||||
if (i < 3) {
|
||||
console.log(`Binary Manager: Trying again`);
|
||||
return this.remove(version, i + 1);
|
||||
console.warn(`Binary Manager: failed to run ${func.name} for ${version}, but failed`, error);
|
||||
if (counter < 4) {
|
||||
console.log(`Binary Manager: Trying again to run ${func.name}`);
|
||||
await rerunner(func, counter + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* General setup, called with a version. Is called during construction
|
||||
* to ensure that we always have or download at least one version.
|
||||
*
|
||||
* @param {string} iVersion
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async setup(iVersion: string): Promise<void> {
|
||||
const version = normalizeVersion(iVersion);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
const { promisify } = await import('util');
|
||||
const eDownload = promisify(require('electron-download'));
|
||||
|
||||
await fs.mkdirp(this.getDownloadPath(version));
|
||||
|
||||
if (this.state[version] === 'downloading') {
|
||||
console.log(`BinaryManager: Electron ${version} already downloading.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.getIsDownloaded(version)) {
|
||||
console.log(`BinaryManager: Electron ${version} already downloaded.`);
|
||||
this.state[version] = 'ready';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`BinaryManager: Electron ${version} not present, downloading`);
|
||||
this.state[version] = 'downloading';
|
||||
|
||||
const zipPath = await eDownload({ version });
|
||||
const extractPath = this.getDownloadPath(version);
|
||||
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking`);
|
||||
|
||||
try {
|
||||
const electronFiles = await this.unzip(zipPath, extractPath);
|
||||
console.log(`Unzipped ${version}`, electronFiles);
|
||||
} catch (error) {
|
||||
console.warn(`Failure while unzipping ${version}`, error);
|
||||
|
||||
// Todo: Handle this case
|
||||
}
|
||||
|
||||
this.state[version] = 'ready';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expected path for the binary of a given Electron version
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} dir
|
||||
* @returns {string}
|
||||
*/
|
||||
public getElectronBinaryPath(
|
||||
version: string,
|
||||
dir: string = this.getDownloadPath(version),
|
||||
): string {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return path.join(dir, 'Electron.app/Contents/MacOS/Electron');
|
||||
case 'freebsd':
|
||||
case 'linux':
|
||||
return path.join(dir, 'electron');
|
||||
case 'win32':
|
||||
return path.join(dir, 'electron.exe');
|
||||
default:
|
||||
throw new Error(`Electron builds are not available for ${process.platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all versions downloaded to disk
|
||||
*
|
||||
* @returns {Promise<Array<string>>}
|
||||
*/
|
||||
public async getDownloadedVersions(): Promise<Array<string>> {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
const downloadPath = path.join(USER_DATA_PATH, 'electron-bin');
|
||||
console.log(`BinaryManager: Checking for downloaded versions`);
|
||||
|
||||
try {
|
||||
const directories = await fs.readdir(downloadPath);
|
||||
const knownVersions: Array<string> = [];
|
||||
|
||||
for (const directory of directories) {
|
||||
if (await this.getIsDownloaded(directory)) {
|
||||
knownVersions.push(directory);
|
||||
}
|
||||
}
|
||||
|
||||
return knownVersions;
|
||||
} catch (error) {
|
||||
console.warn(`Could not read known Electron versions`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Did we already download a given version?
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} dir
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public async getIsDownloaded(version: string, dir?: string): Promise<boolean> {
|
||||
const expectedPath = this.getElectronBinaryPath(version, dir);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
return fs.existsSync(expectedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expected path for a given Electron version
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
private getDownloadPath(version: string): string {
|
||||
return path.join(USER_DATA_PATH, 'electron-bin', version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzips an electron package so that we can actaully use it.
|
||||
*
|
||||
* @param {string} zipPath
|
||||
* @param {string} extractPath
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private unzip(zipPath: string, extractPath: string): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const extract = (await fancyImport<any>('extract-zip')).default;
|
||||
};
|
||||
|
||||
const binaryCleaner = async () => {
|
||||
if (await getIsDownloaded(version)) {
|
||||
// This is necessary since we're messing with .asar files inside
|
||||
// the Electron binaries. Electron, powering Fiddle, will try to
|
||||
// "correct" our calls, but we don't want that right here.
|
||||
process.noAsar = true;
|
||||
await fs.remove(getDownloadPath(version));
|
||||
process.noAsar = false;
|
||||
|
||||
extract(zipPath, { dir: extractPath }, (error: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
isDeleted = true;
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`BinaryManager: Unpacked!`);
|
||||
process.noAsar = false;
|
||||
const typeDefsCleaner = async () => {
|
||||
await removeTypeDefsForVersion(version);
|
||||
};
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await rerunner(binaryCleaner);
|
||||
|
||||
if (isDeleted) {
|
||||
await rerunner(typeDefsCleaner);
|
||||
}
|
||||
}
|
||||
|
||||
/* Did we already download a given version?
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} dir
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export async function getIsDownloaded(version: string, dir?: string): Promise<boolean> {
|
||||
const expectedPath = getElectronBinaryPath(version, dir);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
|
||||
return fs.existsSync(expectedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expected path for the binary of a given Electron version
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} dir
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getElectronBinaryPath(
|
||||
version: string,
|
||||
dir: string = getDownloadPath(version),
|
||||
): string {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return path.join(dir, 'Electron.app/Contents/MacOS/Electron');
|
||||
case 'freebsd':
|
||||
case 'linux':
|
||||
return path.join(dir, 'electron');
|
||||
case 'win32':
|
||||
return path.join(dir, 'electron.exe');
|
||||
default:
|
||||
throw new Error(`Electron builds are not available for ${process.platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getDownloadingVersions(appState: AppState) {
|
||||
return Object.entries(appState.versions)
|
||||
.filter(([_, { state }]) => state === 'downloading')
|
||||
.map(([version, _]) => version);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array of all versions downloaded to disk
|
||||
*
|
||||
* @returns {Promise<Array<string>>}
|
||||
*/
|
||||
export async function getDownloadedVersions(): Promise<Array<string>> {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
const downloadPath = path.join(USER_DATA_PATH, 'electron-bin');
|
||||
console.log(`Binary: Checking for downloaded versions`);
|
||||
|
||||
try {
|
||||
const directories = await fs.readdir(downloadPath);
|
||||
const knownVersions: Array<string> = [];
|
||||
|
||||
for (const directory of directories) {
|
||||
if (await getIsDownloaded(directory)) {
|
||||
knownVersions.push(directory);
|
||||
}
|
||||
}
|
||||
|
||||
return knownVersions;
|
||||
} catch (error) {
|
||||
console.warn(`Could not read known Electron versions`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an Electron version.
|
||||
*
|
||||
* @param {AppState} appState
|
||||
* @param {string} version
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function download(appState: AppState, version: string): Promise<string> {
|
||||
const { download: electronDownload } = await import('@electron/get');
|
||||
const getProgressCallback = (progress: Progress) => {
|
||||
const roundedProgress = Math.round(progress.percent * 100) / 100;
|
||||
|
||||
if (roundedProgress !== appState.versions[version].downloadProgress) {
|
||||
console.debug(`Binary: Version ${version} download progress: ${progress.percent}`);
|
||||
appState.versions[version].downloadProgress = roundedProgress;
|
||||
}
|
||||
};
|
||||
|
||||
if (!appState.versions[version]) {
|
||||
throw new Error(`Version ${version} does not exist in state, cannot download`);
|
||||
}
|
||||
|
||||
const zipFilePath = await electronDownload(version, {
|
||||
downloadOptions: {
|
||||
quiet: true,
|
||||
getProgressCallback
|
||||
}
|
||||
});
|
||||
|
||||
return zipFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expected path for a given Electron version
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
function getDownloadPath(version: string): string {
|
||||
return path.join(USER_DATA_PATH, 'electron-bin', version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzips an electron package so that we can actually use it.
|
||||
*
|
||||
* @param {string} zipPath
|
||||
* @param {string} extractPath
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function unzip(zipPath: string, extractPath: string): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const extract = (await fancyImport<any>('extract-zip')).default;
|
||||
|
||||
process.noAsar = true;
|
||||
|
||||
const options = {
|
||||
dir: extractPath,
|
||||
};
|
||||
|
||||
extract(zipPath, options, (error: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Binary: Unpacked!`);
|
||||
process.noAsar = false;
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface Progress {
|
||||
percent: number;
|
||||
transferred: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { RunnableVersion } from '../interfaces';
|
||||
|
||||
export class Bisector {
|
||||
public revList: Array<RunnableVersion>;
|
||||
public minRev: number;
|
||||
public maxRev: number;
|
||||
private pivot: number;
|
||||
|
||||
constructor(revList: Array<RunnableVersion>) {
|
||||
this.getCurrentVersion = this.getCurrentVersion.bind(this);
|
||||
this.continue = this.continue.bind(this);
|
||||
this.calculatePivot = this.calculatePivot.bind(this);
|
||||
|
||||
this.revList = revList;
|
||||
this.minRev = 0;
|
||||
this.maxRev = revList.length - 1;
|
||||
this.calculatePivot();
|
||||
}
|
||||
|
||||
public getCurrentVersion() {
|
||||
return this.revList[this.pivot];
|
||||
}
|
||||
|
||||
public continue(isGoodVersion: boolean) {
|
||||
let isBisectOver = false;
|
||||
if (this.maxRev - this.minRev <= 1) {
|
||||
isBisectOver = true;
|
||||
}
|
||||
|
||||
if (isGoodVersion) {
|
||||
const upPivot = Math.floor((this.maxRev - this.pivot) / 2) + this.pivot;
|
||||
this.minRev = this.pivot;
|
||||
if (upPivot !== this.maxRev && upPivot !== this.pivot) {
|
||||
this.pivot = upPivot;
|
||||
} else {
|
||||
isBisectOver = true;
|
||||
}
|
||||
} else {
|
||||
const downPivot = Math.floor((this.pivot - this.minRev) / 2) + this.minRev;
|
||||
this.maxRev = this.pivot;
|
||||
if (downPivot !== this.minRev && downPivot !== this.pivot) {
|
||||
this.pivot = downPivot;
|
||||
} else {
|
||||
isBisectOver = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isBisectOver) {
|
||||
return [this.revList[this.minRev], this.revList[this.maxRev]];
|
||||
} else {
|
||||
return this.revList[this.pivot];
|
||||
}
|
||||
}
|
||||
|
||||
private calculatePivot() {
|
||||
this.pivot = Math.floor((this.maxRev - this.minRev) / 2);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { remote } from 'electron';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
@@ -10,11 +11,25 @@ export interface ChromeMacProps {
|
||||
|
||||
@observer
|
||||
export class ChromeMac extends React.Component<ChromeMacProps> {
|
||||
public handleDoubleClick = () => {
|
||||
const doubleClickAction = remote.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
|
||||
const win = remote.getCurrentWindow();
|
||||
if (doubleClickAction === 'Minimize') {
|
||||
win.minimize();
|
||||
} else if (doubleClickAction === 'Maximize') {
|
||||
if (!win.isMaximized()) {
|
||||
win.maximize();
|
||||
} else {
|
||||
win.unmaximize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (process.platform !== 'darwin') return null;
|
||||
|
||||
return (
|
||||
<div className='chrome drag'>
|
||||
<div className='chrome drag' onDoubleClick={this.handleDoubleClick}>
|
||||
<small>{getTitle(this.props.appState)}</small>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Button, InputGroup, Intent } from '@blueprintjs/core';
|
||||
import classnames from 'classnames';
|
||||
import { reaction } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../constants';
|
||||
import { classNames } from '../../utils/classnames';
|
||||
import { getTitle } from '../../utils/get-title';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { idFromUrl, urlFromId } from '../../utils/gist';
|
||||
import { getOctokit } from '../../utils/octokit';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface AddressBarProps {
|
||||
@@ -16,21 +15,32 @@ export interface AddressBarProps {
|
||||
|
||||
export interface AddressBarState {
|
||||
value: string;
|
||||
loaders: {
|
||||
gist: any;
|
||||
example: any;
|
||||
};
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AddressBar extends React.Component<AddressBarProps, AddressBarState> {
|
||||
constructor(props: AddressBarProps) {
|
||||
super(props);
|
||||
|
||||
this.loadFiddle = this.loadFiddle.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.submit = this.submit.bind(this);
|
||||
|
||||
const { gistId } = this.props.appState;
|
||||
const value = gistId ? urlFromId(gistId) : '';
|
||||
|
||||
this.state = { value };
|
||||
const { remoteLoader } = window.ElectronFiddle.app;
|
||||
|
||||
this.state = {
|
||||
value,
|
||||
loaders: {
|
||||
gist: remoteLoader.loadFiddleFromGist.bind(remoteLoader),
|
||||
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,15 +49,20 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
|
||||
*
|
||||
* @param {React.SyntheticEvent<HTMLFormElement>} event
|
||||
*/
|
||||
public async handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
|
||||
public handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
|
||||
this.props.appState.gistId = idFromUrl(this.state.value) || this.state.value;
|
||||
|
||||
console.log('Loading');
|
||||
|
||||
/**
|
||||
* Commit the address bar's value to app state and load the fiddle.
|
||||
*
|
||||
* @memberof AddressBar
|
||||
*/
|
||||
public submit() {
|
||||
const { remoteLoader } = window.ElectronFiddle.app;
|
||||
if (this.state.value) {
|
||||
this.loadFiddle();
|
||||
remoteLoader.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +70,20 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
|
||||
* Once the component mounts, we'll subscribe to gistId changes
|
||||
*/
|
||||
public componentDidMount() {
|
||||
const { appState } = this.props;
|
||||
const { loaders } = this.state;
|
||||
reaction(
|
||||
() => this.props.appState.gistId,
|
||||
() => appState.gistId,
|
||||
(gistId: string) => this.setState({ value: urlFromId(gistId) })
|
||||
);
|
||||
ipcRendererManager.on(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
|
||||
ipcRendererManager.on(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const { loaders } = this.state;
|
||||
ipcRendererManager.removeListener(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
|
||||
ipcRendererManager.removeListener(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,69 +95,36 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
|
||||
this.setState({ value: event.target.value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a fiddle
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* @memberof AddressBar
|
||||
*/
|
||||
public async loadFiddle(): Promise<boolean> {
|
||||
const { appState } = this.props;
|
||||
|
||||
try {
|
||||
const octo = await getOctokit();
|
||||
const gist = await octo.gists.get({
|
||||
gist_id: appState.gistId
|
||||
});
|
||||
|
||||
this.props.appState.setWarningDialogTexts({
|
||||
label: 'Loading the fiddle will replace your current unsaved changes. Do you want to discard them?'
|
||||
});
|
||||
|
||||
await window.ElectronFiddle.app.setValues({
|
||||
html: gist.data.files[INDEX_HTML_NAME].content,
|
||||
main: gist.data.files[MAIN_JS_NAME].content,
|
||||
renderer: gist.data.files[RENDERER_JS_NAME].content,
|
||||
});
|
||||
|
||||
document.title = getTitle(appState);
|
||||
appState.localPath = null;
|
||||
} catch (error) {
|
||||
console.warn(`Loading Fiddle failed`, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public renderLoadButton(isValueCorrect: boolean): JSX.Element {
|
||||
return (
|
||||
<Button
|
||||
disabled={!isValueCorrect}
|
||||
icon='cloud-download'
|
||||
text='Load Fiddle'
|
||||
onClick={this.loadFiddle}
|
||||
onClick={this.submit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isUnsaved } = this.props.appState;
|
||||
const { isUnsaved, isPublishing } = this.props.appState;
|
||||
const { value } = this.state;
|
||||
const isCorrect = /https:\/\/gist\.github\.com\/(.+)$/.test(value);
|
||||
const className = classNames('address-bar', isUnsaved, { empty: !value });
|
||||
const className = classnames('address-bar', isUnsaved, { empty: !value });
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={this.handleSubmit}>
|
||||
<InputGroup
|
||||
key='addressbar'
|
||||
leftIcon='geosearch'
|
||||
intent={isCorrect || !value ? undefined : Intent.DANGER}
|
||||
onChange={this.handleChange}
|
||||
placeholder='https://gist.github.com/...'
|
||||
value={value}
|
||||
rightElement={this.renderLoadButton(isCorrect)}
|
||||
/>
|
||||
<fieldset disabled={isPublishing}>
|
||||
<InputGroup
|
||||
key='addressbar'
|
||||
leftIcon='geosearch'
|
||||
intent={isCorrect || !value ? undefined : Intent.DANGER}
|
||||
onChange={this.handleChange}
|
||||
placeholder='https://gist.github.com/...'
|
||||
value={value}
|
||||
rightElement={this.renderLoadButton(isCorrect)}
|
||||
/>
|
||||
</fieldset>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GenericDialogType, VersionState } from '../../../src/interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
interface BisectHandlerProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class BisectHandler extends React.Component<BisectHandlerProps> {
|
||||
constructor(props: BisectHandlerProps) {
|
||||
super(props);
|
||||
|
||||
this.continueBisect = this.continueBisect.bind(this);
|
||||
this.terminateBisect = this.terminateBisect.bind(this);
|
||||
}
|
||||
|
||||
public continueBisect(isGood: boolean) {
|
||||
window.ElectronFiddle.app.runner.stop();
|
||||
|
||||
const { appState } = this.props;
|
||||
const response = appState.Bisector!.continue(isGood);
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
this.terminateBisect();
|
||||
|
||||
const [minRev, maxRev] = response;
|
||||
const [minVer, maxVer] = [minRev.version, maxRev.version];
|
||||
const message = `Check between versions ${minVer} and ${maxVer}.`;
|
||||
|
||||
appState.pushOutput(`[BISECT] Complete: ${message}`);
|
||||
appState.setGenericDialogOptions({
|
||||
type: GenericDialogType.success,
|
||||
label: `Bisect complete. ${message}`,
|
||||
cancel: undefined
|
||||
});
|
||||
appState.isGenericDialogShowing = true;
|
||||
} else {
|
||||
appState.setVersion(response.version);
|
||||
}
|
||||
}
|
||||
|
||||
public terminateBisect() {
|
||||
const { appState } = this.props;
|
||||
appState.Bisector = undefined;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
if (!!appState.Bisector) {
|
||||
const isDownloading = appState.currentElectronVersion.state === VersionState.downloading;
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon={'thumbs-up'}
|
||||
onClick={() => this.continueBisect(true)}
|
||||
disabled={isDownloading}
|
||||
/>
|
||||
<Button
|
||||
icon={'thumbs-down'}
|
||||
onClick={() => this.continueBisect(false)}
|
||||
disabled={isDownloading}
|
||||
/>
|
||||
<Button
|
||||
icon={'cross'}
|
||||
onClick={this.terminateBisect}
|
||||
>
|
||||
Cancel Bisect
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
icon='git-branch'
|
||||
text='Bisect'
|
||||
onClick={appState.toggleBisectDialog}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Button, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ALL_EDITORS, MosaicId, PanelId } from '../../interfaces';
|
||||
import { getVisibleMosaics } from '../../utils/editors-mosaic-arrangement';
|
||||
import { AppState } from '../state';
|
||||
import { TITLE_MAP } from './editors';
|
||||
|
||||
export interface EditorDropdownState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EditorDropdownProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dropdown allowing users to toggle the various editors
|
||||
*
|
||||
* @class EditorDropdown
|
||||
* @extends {React.Component<EditorDropdownProps, EditorDropdownState>}
|
||||
*/
|
||||
@observer
|
||||
export class EditorDropdown extends React.Component<EditorDropdownProps, EditorDropdownState> {
|
||||
constructor(props: EditorDropdownProps) {
|
||||
super(props);
|
||||
|
||||
this.onItemClick = this.onItemClick.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Popover content={this.renderMenu()} position={Position.BOTTOM}>
|
||||
<Button icon='applications' text='Editors' />
|
||||
</Popover>
|
||||
{this.renderDocsDemos()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderDocsDemos() {
|
||||
if (!process.env.FIDDLE_DOCS_DEMOS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon='help'
|
||||
text='Docs & Demos'
|
||||
id={PanelId.docsDemo}
|
||||
onClick={this.onItemClick}
|
||||
active={!this.props.appState.closedPanels.docsDemo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMenu() {
|
||||
return (
|
||||
<Menu>
|
||||
{...this.renderMenuItems()}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMenuItems() {
|
||||
const { appState } = this.props;
|
||||
const result: Array<JSX.Element> = [];
|
||||
const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
|
||||
|
||||
for (const id of ALL_EDITORS) {
|
||||
result.push(
|
||||
<MenuItem
|
||||
icon={visibleMosaics.includes(id) ? 'eye-open' : 'eye-off'}
|
||||
key={id}
|
||||
text={TITLE_MAP[id]}
|
||||
id={id}
|
||||
onClick={this.onItemClick}
|
||||
disabled={appState.mosaicArrangement === id} // can't hide last editor panel
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
result.push(
|
||||
<React.Fragment key={'fragment-reset-layout'}>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon='grid-view'
|
||||
key='reset-layout'
|
||||
text='Reset Layout'
|
||||
onClick={appState.resetEditorLayout}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public onItemClick(event: React.MouseEvent) {
|
||||
const { id } = event.currentTarget;
|
||||
const { appState } = this.props;
|
||||
const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
|
||||
|
||||
if (visibleMosaics.includes(id as MosaicId)) {
|
||||
console.log(`EditorDropdown: Closing ${id}`);
|
||||
appState.hideAndBackupMosaic(id as MosaicId);
|
||||
} else {
|
||||
console.log(`EditorDropdown: Opening ${id}`);
|
||||
appState.showMosaic(id as MosaicId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Button, ButtonGroup, Menu, MenuItem, Popover, Position } from '@blueprintjs/core';
|
||||
import { Button, ButtonGroup, IToastProps, Menu, MenuItem, Popover, Position, Toaster } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { when } from 'mobx';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../constants';
|
||||
import { EditorValues } from '../../interfaces';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../../shared-constants';
|
||||
import { getOctokit } from '../../utils/octokit';
|
||||
import { EMPTY_EDITOR_CONTENT } from '../constants';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
|
||||
@@ -13,8 +15,9 @@ export interface PublishButtonProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface PublishButtonState {
|
||||
isPublishing: boolean;
|
||||
interface IPublishButtonState {
|
||||
readonly isUpdating: boolean;
|
||||
readonly wouldPublish: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,19 +28,33 @@ export interface PublishButtonState {
|
||||
* @extends {React.Component<PublishButtonProps, PublishButtonState>}
|
||||
*/
|
||||
@observer
|
||||
export class PublishButton extends React.Component<PublishButtonProps, PublishButtonState> {
|
||||
constructor(props: PublishButtonProps) {
|
||||
export class PublishButton extends React.Component<PublishButtonProps, IPublishButtonState> {
|
||||
public constructor(props: PublishButtonProps) {
|
||||
super(props);
|
||||
|
||||
this.state = { isPublishing: false };
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.publishFiddle = this.publishFiddle.bind(this);
|
||||
this.publishOrUpdateFiddle = this.publishOrUpdateFiddle.bind(this);
|
||||
this.setPrivate = this.setPrivate.bind(this);
|
||||
this.setPublic = this.setPublic.bind(this);
|
||||
|
||||
this.state = {
|
||||
isUpdating: false,
|
||||
wouldPublish: false,
|
||||
};
|
||||
}
|
||||
|
||||
private toaster: Toaster;
|
||||
private refHandlers = {
|
||||
toaster: (ref: Toaster) => this.toaster = ref,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
ipcRendererManager.off(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user clicks the publish button, we either show the
|
||||
* authentication dialog or publish right away.
|
||||
@@ -60,7 +77,7 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
await when(() => !!appState.gitHubToken || !appState.isTokenDialogShowing);
|
||||
|
||||
if (appState.gitHubToken) {
|
||||
return this.publishFiddle();
|
||||
return this.publishOrUpdateFiddle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,60 +85,147 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
* Connect with GitHub, publish the current Fiddle as a gist,
|
||||
* and update all related properties in the app state.
|
||||
*/
|
||||
public async publishFiddle(): Promise<void> {
|
||||
this.setState({ isPublishing: true });
|
||||
public async publishOrUpdateFiddle(): Promise<void> {
|
||||
const { appState } = this.props;
|
||||
const { wouldPublish } = this.state;
|
||||
appState.isPublishing = true;
|
||||
|
||||
const octo = await getOctokit();
|
||||
octo.authenticate({
|
||||
type: 'token',
|
||||
token: this.props.appState.gitHubToken!
|
||||
});
|
||||
|
||||
const { gitHubPublishAsPublic } = this.props.appState;
|
||||
const octo = await getOctokit(this.props.appState);
|
||||
const { gitHubPublishAsPublic, gistId } = this.props.appState;
|
||||
const options = { includeDependencies: true, includeElectron: true };
|
||||
const values = await window.ElectronFiddle.app.getValues(options);
|
||||
const values = await window.ElectronFiddle.app.getEditorValues(options);
|
||||
|
||||
try {
|
||||
const gist = await octo.gists.create({
|
||||
public: !!gitHubPublishAsPublic,
|
||||
description: 'Electron Fiddle Gist',
|
||||
files: {
|
||||
[INDEX_HTML_NAME]: {
|
||||
content: values.html || '<!-- Empty -->',
|
||||
},
|
||||
[MAIN_JS_NAME]: {
|
||||
content: values.main || '// Empty',
|
||||
},
|
||||
[RENDERER_JS_NAME]: {
|
||||
content: values.renderer || '// Empty',
|
||||
},
|
||||
},
|
||||
} as any); // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
|
||||
if (gistId && !wouldPublish) {
|
||||
this.setState({
|
||||
isUpdating: true
|
||||
});
|
||||
const gist = await octo.gists.update({
|
||||
gist_id: appState.gistId,
|
||||
files: this.gistFilesList(values) as any,
|
||||
});
|
||||
|
||||
this.props.appState.gistId = gist.data.id;
|
||||
console.log('Updating: Updating done', { gist });
|
||||
this.renderToast({ message: 'Successfully updated gist!' });
|
||||
this.setState({
|
||||
isUpdating: false
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const gist = await octo.gists.create({
|
||||
public: !!gitHubPublishAsPublic,
|
||||
description: 'Electron Fiddle Gist',
|
||||
files: this.gistFilesList(values) as any, // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
|
||||
});
|
||||
|
||||
console.log(`Publish Button: Publishing done`, { gist });
|
||||
} catch (error) {
|
||||
console.warn(`Could not publish gist`, { error });
|
||||
appState.gistId = gist.data.id;
|
||||
|
||||
const messageBoxOptions: Electron.MessageBoxOptions = {
|
||||
message: 'Publishing Fiddle to GitHub failed. Are you connected to the Internet?',
|
||||
detail: `GitHub encountered the following error: ${error.message}`
|
||||
};
|
||||
console.log(`Publish Button: Publishing done`, { gist });
|
||||
this.renderToast({ message: 'Publishing done successfully!' });
|
||||
} catch (error) {
|
||||
console.warn(`Could not publish gist`, { error });
|
||||
|
||||
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
|
||||
const messageBoxOptions: Electron.MessageBoxOptions = {
|
||||
message: 'Publishing Fiddle to GitHub failed. Are you connected to the Internet?',
|
||||
detail: `GitHub encountered the following error: ${error.message}`
|
||||
};
|
||||
|
||||
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ isPublishing: false });
|
||||
appState.isPublishing = false;
|
||||
}
|
||||
|
||||
public setPrivacy(publishAsPublic: boolean) {
|
||||
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
|
||||
/**
|
||||
* Publish fiddles as private.
|
||||
*
|
||||
* @memberof PublishButton
|
||||
*/
|
||||
public setPrivate() {
|
||||
this.setPrivacy(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish fiddles as public.
|
||||
*
|
||||
* @memberof PublishButton
|
||||
*/
|
||||
public setPublic() {
|
||||
this.setPrivacy(true);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { gitHubPublishAsPublic } = this.props.appState;
|
||||
const { isPublishing } = this.state;
|
||||
const { isPublishing, gistId } = this.props.appState;
|
||||
const { isUpdating, wouldPublish } = this.state;
|
||||
|
||||
const getTextForButton = gistId && !wouldPublish
|
||||
? 'Update'
|
||||
: isUpdating
|
||||
? 'Updating...'
|
||||
: isPublishing
|
||||
? 'Publishing...'
|
||||
: 'Publish';
|
||||
|
||||
return (
|
||||
<>
|
||||
<fieldset disabled={isPublishing}>
|
||||
<ButtonGroup className='button-publish'>
|
||||
{this.renderPrivaryMenu()}
|
||||
<Button
|
||||
onClick={this.handleClick}
|
||||
loading={isPublishing}
|
||||
icon='upload'
|
||||
text={getTextForButton}
|
||||
/>
|
||||
{this.renderMaybePublishMenu()}
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
<Toaster position={Position.BOTTOM_RIGHT} ref={this.refHandlers.toaster} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMaybePublishMenu = () => {
|
||||
const { gistId } = this.props.appState;
|
||||
const { wouldPublish } = this.state;
|
||||
|
||||
if (!gistId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text='Publish'
|
||||
active={wouldPublish}
|
||||
onClick={() => this.setWouldPublish(true)}
|
||||
/>
|
||||
<MenuItem
|
||||
text='Update'
|
||||
active={!wouldPublish}
|
||||
onClick={() => this.setWouldPublish(false)}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={menu}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
icon='wrench'
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPrivaryMenu = () => {
|
||||
const { gitHubPublishAsPublic, gistId } = this.props.appState;
|
||||
|
||||
if (gistId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
|
||||
const privacyMenu = (
|
||||
@@ -130,36 +234,60 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
text='Private'
|
||||
icon='lock'
|
||||
active={!gitHubPublishAsPublic}
|
||||
onClick={() => this.setPrivacy(false)}
|
||||
onClick={this.setPrivate}
|
||||
/>
|
||||
<MenuItem
|
||||
text='Public'
|
||||
icon='unlock'
|
||||
active={gitHubPublishAsPublic}
|
||||
onClick={() => this.setPrivacy(true)}
|
||||
onClick={this.setPublic}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Popover
|
||||
content={privacyMenu}
|
||||
position={Position.BOTTOM}
|
||||
disabled={isPublishing}
|
||||
>
|
||||
<Button
|
||||
icon={privacyIcon}
|
||||
/>
|
||||
</Popover>
|
||||
<Popover
|
||||
content={privacyMenu}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
onClick={this.handleClick}
|
||||
loading={isPublishing}
|
||||
disabled={isPublishing}
|
||||
icon='upload'
|
||||
text={isPublishing ? 'Publishing...' : 'Publish'}
|
||||
icon={privacyIcon}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
private setWouldPublish = (wouldPublish: boolean) => {
|
||||
this.setState({
|
||||
wouldPublish,
|
||||
});
|
||||
}
|
||||
|
||||
private setPrivacy(publishAsPublic: boolean) {
|
||||
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
|
||||
}
|
||||
|
||||
private renderToast = (toast: IToastProps) => {
|
||||
this.toaster.show(toast);
|
||||
}
|
||||
|
||||
private gistFilesList = (values: EditorValues) => {
|
||||
return {
|
||||
[INDEX_HTML_NAME]: {
|
||||
content: values.html || EMPTY_EDITOR_CONTENT.html,
|
||||
},
|
||||
[MAIN_JS_NAME]: {
|
||||
content: values.main || EMPTY_EDITOR_CONTENT.js,
|
||||
},
|
||||
[RENDERER_JS_NAME]: {
|
||||
content: values.renderer || EMPTY_EDITOR_CONTENT.js,
|
||||
},
|
||||
[PRELOAD_JS_NAME]: {
|
||||
content: values.preload || EMPTY_EDITOR_CONTENT.js,
|
||||
},
|
||||
[STYLES_CSS_NAME]: {
|
||||
content: values.css || EMPTY_EDITOR_CONTENT.css,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
import { Button, IButtonProps } from '@blueprintjs/core';
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import { Button, IButtonProps, Spinner } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
|
||||
import { EditorValues, ElectronVersionSource, ElectronVersionState, FileTransform } from '../../interfaces';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { PackageJsonOptions } from '../../utils/get-package';
|
||||
import { normalizeVersion } from '../../utils/normalize-version';
|
||||
import { maybePlural } from '../../utils/plural-maybe';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { findModulesInEditors, getIsNpmInstalled, installModules, npmRun } from '../npm';
|
||||
import { VersionState } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface RunnerState {
|
||||
isRunning: boolean;
|
||||
}
|
||||
|
||||
export interface RunnerProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export enum ForgeCommands {
|
||||
PACKAGE = 'package',
|
||||
MAKE = 'make'
|
||||
}
|
||||
|
||||
/**
|
||||
* The runner component is responsible for actually launching the fiddle
|
||||
* with Electron. It also renders the button that does so.
|
||||
@@ -35,279 +21,36 @@ export enum ForgeCommands {
|
||||
*/
|
||||
@observer
|
||||
export class Runner extends React.Component<RunnerProps, RunnerState> {
|
||||
public child: ChildProcess | null = null;
|
||||
|
||||
constructor(props: RunnerProps) {
|
||||
super(props);
|
||||
|
||||
this.run = this.run.bind(this);
|
||||
this.performForgeOperation = this.performForgeOperation.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
this.state = { isRunning: false };
|
||||
|
||||
this.props.appState.pushOutput('Console ready 🔬');
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
ipcRendererManager.on(IpcEvents.FIDDLE_RUN, this.run);
|
||||
ipcRendererManager.on(IpcEvents.FIDDLE_PACKAGE, () => {
|
||||
this.performForgeOperation(ForgeCommands.PACKAGE);
|
||||
});
|
||||
ipcRendererManager.on(IpcEvents.FIDDLE_MAKE, () => {
|
||||
this.performForgeOperation(ForgeCommands.MAKE);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { versions, version } = this.props.appState;
|
||||
const { isRunning } = this.state;
|
||||
const { isRunning, currentElectronVersion } = this.props.appState;
|
||||
|
||||
if (!versions || !version || !versions[normalizeVersion(version)]) {
|
||||
return null;
|
||||
}
|
||||
const state = currentElectronVersion && currentElectronVersion.state;
|
||||
const props: IButtonProps = { className: 'button-run', disabled: true };
|
||||
|
||||
const state = versions[normalizeVersion(version)].state;
|
||||
const props: IButtonProps = {};
|
||||
|
||||
if (state === ElectronVersionState.downloading) {
|
||||
if (state === VersionState.downloading) {
|
||||
props.text = 'Downloading';
|
||||
props.disabled = true;
|
||||
props.loading = true;
|
||||
} else if (state === ElectronVersionState.ready) {
|
||||
props.icon = <Spinner size={16} value={currentElectronVersion?.downloadProgress} />;
|
||||
} else if (state === VersionState.unzipping) {
|
||||
props.text = 'Unzipping';
|
||||
props.icon = <Spinner size={16} />;
|
||||
} else if (state === VersionState.ready) {
|
||||
props.disabled = false;
|
||||
|
||||
if (isRunning) {
|
||||
props.active = true;
|
||||
props.text = 'Stop';
|
||||
props.onClick = this.stop;
|
||||
props.onClick = window.ElectronFiddle.app.runner.stop;
|
||||
props.icon = 'stop';
|
||||
} else {
|
||||
props.text = 'Run';
|
||||
props.onClick = this.run;
|
||||
props.onClick = window.ElectronFiddle.app.runner.run;
|
||||
props.icon = 'play';
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
props.text = 'Checking status';
|
||||
props.icon = <Spinner size={16} />;
|
||||
}
|
||||
|
||||
return <Button {...props} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a currently running Electron fiddle.
|
||||
*/
|
||||
public async stop() {
|
||||
if (this.child) {
|
||||
this.child.kill();
|
||||
this.setState({
|
||||
isRunning: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the editor's JavaScript contents for modules
|
||||
* and installs them.
|
||||
*
|
||||
* @param {EditorValues} values
|
||||
* @param {string} dir
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async installModulesForEditor(values: EditorValues, dir: string): Promise<void> {
|
||||
const modules = await findModulesInEditors(values);
|
||||
const { appState } = this.props;
|
||||
|
||||
if (modules && modules.length > 0) {
|
||||
if (!(await getIsNpmInstalled())) {
|
||||
let message = `The ${maybePlural(`module`, modules)} ${modules.join(', ')} need to be installed, `;
|
||||
message += `but we could not find npm. Fiddle requires Node.js and npm `;
|
||||
message += `to support the installation of modules not included in `;
|
||||
message += `Electron. Please visit https://nodejs.org to install Node.js `;
|
||||
message += `and npm.`;
|
||||
|
||||
appState.pushOutput(message, { isNotPre: true });
|
||||
return;
|
||||
}
|
||||
|
||||
appState.pushOutput(`Installing npm modules: ${modules.join(', ')}...`, { isNotPre: true });
|
||||
appState.pushOutput(await installModules({ dir }, ...modules));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Electron.
|
||||
*
|
||||
* @param {string} dir
|
||||
* @param {string} version
|
||||
* @returns {Promise<void>}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async execute(dir: string): Promise<void> {
|
||||
const { appState } = this.props;
|
||||
const { version, pushOutput, versions } = appState;
|
||||
|
||||
const isLocal = versions[version] && versions[version].source === ElectronVersionSource.local;
|
||||
const electronDir = isLocal ? versions[version].localPath : undefined;
|
||||
const binaryPath = appState.binaryManager.getElectronBinaryPath(version, electronDir);
|
||||
console.log(`Runner: Binary ${binaryPath} ready, launching`);
|
||||
|
||||
this.child = spawn(binaryPath, [ dir, '--inspect' ]);
|
||||
this.setState({ isRunning: true });
|
||||
pushOutput(`Electron v${version} started.`);
|
||||
|
||||
this.child.stdout.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
|
||||
this.child.stderr.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
|
||||
this.child.on('close', (code) => {
|
||||
const withCode = typeof code === 'number'
|
||||
? ` with code ${code.toString()}.`
|
||||
: `.`;
|
||||
|
||||
pushOutput(`Electron exited${withCode}`);
|
||||
this.setState({ isRunning: false });
|
||||
this.child = null;
|
||||
window.ElectronFiddle.app.fileManager.cleanup(dir);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save files to temp, logging to the Fiddle terminal while doing so
|
||||
*
|
||||
* @param {PackageJsonOptions} options
|
||||
* @param {...Array<FileTransform>} transforms
|
||||
* @returns {(Promise<string | null>)}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async saveToTemp(
|
||||
options: PackageJsonOptions, ...transforms: Array<FileTransform>
|
||||
): Promise<string | null> {
|
||||
const { fileManager } = window.ElectronFiddle.app;
|
||||
const { pushOutput, pushError } = this.props.appState;
|
||||
|
||||
try {
|
||||
pushOutput(`Saving files to temp directory...`);
|
||||
const dir = await fileManager.saveToTemp(options, ...transforms);
|
||||
pushOutput(`Saved files to ${dir}`);
|
||||
return dir;
|
||||
} catch (error) {
|
||||
pushError('Failed to save files.', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs modules in a given directory (we're basically
|
||||
* just running "npm install")
|
||||
*
|
||||
* @param {string} dir
|
||||
* @returns
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async npmInstall(dir: string): Promise<boolean> {
|
||||
try {
|
||||
this.props.appState.pushOutput(`Now running "npm install..."`);
|
||||
this.props.appState.pushOutput(await installModules({ dir }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.props.appState.pushError('Failed to run "npm install".', error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses electron-forge to either package or make the current fiddle
|
||||
*
|
||||
* @param {ForgeCommands} operation
|
||||
* @returns {Promise<boolean>}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async performForgeOperation(operation: ForgeCommands): Promise<boolean> {
|
||||
const options = { includeDependencies: true, includeElectron: true };
|
||||
const { dotfilesTransform } = await import('../transforms/dotfiles');
|
||||
const { forgeTransform } = await import('../transforms/forge');
|
||||
const { appState } = this.props;
|
||||
const { pushError, pushOutput } = appState;
|
||||
|
||||
const strings = operation === ForgeCommands.MAKE
|
||||
? [ 'Creating installers for', 'Binary' ]
|
||||
: [ 'Packaging', 'Installers' ];
|
||||
|
||||
appState.isConsoleShowing = true;
|
||||
pushOutput(`📦 ${strings[0]} current Fiddle...`);
|
||||
|
||||
if (!(await getIsNpmInstalled())) {
|
||||
let message = `Error: Could not find npm. Fiddle requires Node.js and npm `;
|
||||
message += `to compile packages. Please visit https://nodejs.org to install `;
|
||||
message += `Node.js and npm.`;
|
||||
|
||||
appState.pushOutput(message, { isNotPre: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save files to temp
|
||||
const dir = await this.saveToTemp(options, dotfilesTransform, forgeTransform);
|
||||
if (!dir) return false;
|
||||
|
||||
// Files are now saved to temp, let's install Forge and dependencies
|
||||
if (!(await this.npmInstall(dir))) return false;
|
||||
|
||||
// Cool, let's run "package"
|
||||
try {
|
||||
console.log(`Now creating ${strings[1].toLowerCase()}...`);
|
||||
pushOutput(await npmRun({ dir }, operation));
|
||||
pushOutput(`✅ ${strings[1]} successfully created.`, { isNotPre: true });
|
||||
} catch (error) {
|
||||
pushError(`Creating ${strings[1].toLowerCase()} failed.`, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { shell } = await import('electron');
|
||||
shell.showItemInFolder(path.join(dir, 'out'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually run the fiddle.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public async run(): Promise<boolean> {
|
||||
const { appState } = this.props;
|
||||
const { fileManager, getValues } = window.ElectronFiddle.app;
|
||||
const options = { includeDependencies: false, includeElectron: false };
|
||||
const { binaryManager, version, versions } = appState;
|
||||
|
||||
appState.isConsoleShowing = true;
|
||||
|
||||
const isLocal = versions[version] && versions[version].source === ElectronVersionSource.local;
|
||||
const isDownloaded = isLocal || await binaryManager.getIsDownloaded(version);
|
||||
const values = await getValues(options);
|
||||
const dir = await this.saveToTemp(options);
|
||||
|
||||
if (!dir) return false;
|
||||
|
||||
try {
|
||||
await this.installModulesForEditor(values, dir);
|
||||
} catch (error) {
|
||||
console.error('Runner: Could not install modules', error);
|
||||
fileManager.cleanup(dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isDownloaded) {
|
||||
console.warn(`Runner: Binary ${version} not ready`);
|
||||
|
||||
let message = `Could not start fiddle: `;
|
||||
message += `Electron ${version} not downloaded yet. `;
|
||||
message += `Please wait for it to finish downloading `;
|
||||
message += `before running the fiddle.`;
|
||||
|
||||
appState.pushOutput(message, { isNotPre: true });
|
||||
fileManager.cleanup(dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.execute(dir);
|
||||
|
||||
return true;
|
||||
return <Button {...props} type={undefined} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,9 @@
|
||||
import { Button, MenuItem } from '@blueprintjs/core';
|
||||
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
|
||||
import { ButtonGroup } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
|
||||
import { highlightText } from '../../utils/highlight-text';
|
||||
import { sortedElectronMap } from '../../utils/sorted-electron-map';
|
||||
import { AppState } from '../state';
|
||||
import { getReleaseChannel } from '../versions';
|
||||
|
||||
const ElectronVersionSelect = Select.ofType<ElectronVersion>();
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> label for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {ElectronVersion} { source, state }
|
||||
* @returns {string}
|
||||
*/
|
||||
function getItemLabel({ source, state }: ElectronVersion): string {
|
||||
let label = '';
|
||||
|
||||
if (source === ElectronVersionSource.local) {
|
||||
label = name;
|
||||
} else {
|
||||
if (state === ElectronVersionState.unknown) {
|
||||
label = `Not downloaded`;
|
||||
} else if (state === ElectronVersionState.ready) {
|
||||
label = `Downloaded`;
|
||||
} else if (state === ElectronVersionState.downloading) {
|
||||
label = `Downloading`;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> icon for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {ElectronVersion} { state }
|
||||
* @returns
|
||||
*/
|
||||
function getItemIcon({ state }: ElectronVersion) {
|
||||
return state === 'ready'
|
||||
? 'saved'
|
||||
: state === 'downloading' ? 'cloud-download' : 'cloud';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> predicate for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {string} query
|
||||
* @param {ElectronVersion} { version }
|
||||
* @returns
|
||||
*/
|
||||
const filterItem: ItemPredicate<ElectronVersion> = (query, { version }) => {
|
||||
return version.toLowerCase().includes(query.toLowerCase());
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> <MenuItem /> for Electron
|
||||
* versions.
|
||||
*
|
||||
* @param {ElectronVersion} item
|
||||
* @param {IItemRendererProps} { handleClick, modifiers, query }
|
||||
* @returns
|
||||
*/
|
||||
const renderItem: ItemRenderer<ElectronVersion> = (item, { handleClick, modifiers, query }) => {
|
||||
if (!modifiers.matchesPredicate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
text={highlightText(item.version, query)}
|
||||
key={item.version}
|
||||
onClick={handleClick}
|
||||
label={getItemLabel(item)}
|
||||
icon={getItemIcon(item)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface VersionChooserState {
|
||||
value: string;
|
||||
}
|
||||
import { VersionSelect } from './version-select';
|
||||
|
||||
export interface VersionChooserProps {
|
||||
appState: AppState;
|
||||
@@ -98,56 +12,18 @@ export interface VersionChooserProps {
|
||||
/**
|
||||
* A dropdown allowing the selection of Electron versions. The actual
|
||||
* download is managed in the state.
|
||||
*
|
||||
* @class VersionChooser
|
||||
* @extends {React.Component<VersionChooserProps, VersionChooserState>}
|
||||
*/
|
||||
@observer
|
||||
export class VersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
|
||||
constructor(props: VersionChooserProps) {
|
||||
super(props);
|
||||
export const VersionChooser = observer((props: VersionChooserProps) => {
|
||||
const { currentElectronVersion, Bisector, setVersion } = props.appState;
|
||||
|
||||
this.onItemSelect = this.onItemSelect.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle change, which usually means that we'd like update
|
||||
* the selection version.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLSelectElement>} event
|
||||
*/
|
||||
public onItemSelect({ version }: ElectronVersion) {
|
||||
this.props.appState.setVersion(version);
|
||||
}
|
||||
|
||||
public getItems(): Array<ElectronVersion> {
|
||||
const { versions, versionsToShow } = this.props.appState;
|
||||
|
||||
return sortedElectronMap<ElectronVersion>(versions, (_key, item) => item)
|
||||
.filter((item) => !!item && versionsToShow.includes(getReleaseChannel(item)));
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { version, versions } = this.props.appState;
|
||||
const selectedVersion = versions[version];
|
||||
|
||||
if (!selectedVersion) return null;
|
||||
|
||||
return (
|
||||
<ElectronVersionSelect
|
||||
filterable={true}
|
||||
items={this.getItems()}
|
||||
itemRenderer={renderItem}
|
||||
itemPredicate={filterItem}
|
||||
onItemSelect={this.onItemSelect}
|
||||
noResults={<MenuItem disabled={true} text='No results.' />}
|
||||
>
|
||||
<Button
|
||||
className='version-chooser'
|
||||
text={`Electron v${version}`}
|
||||
icon={getItemIcon(selectedVersion)}
|
||||
/>
|
||||
</ElectronVersionSelect>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<VersionSelect
|
||||
appState={props.appState}
|
||||
onVersionSelect={({ version }) => setVersion(version)}
|
||||
currentVersion={currentElectronVersion}
|
||||
disabled={!!Bisector}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { AddressBar } from './commands-address-bar';
|
||||
import { BisectHandler } from './commands-bisect';
|
||||
import { EditorDropdown } from './commands-editors';
|
||||
import { PublishButton } from './commands-publish-button';
|
||||
import { Runner } from './commands-runner';
|
||||
import { VersionChooser } from './commands-version-chooser';
|
||||
@@ -27,6 +29,7 @@ export class Commands extends React.Component<CommandsProps, {}> {
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { isBisectCommandShowing: isBisectCommandShowing } = appState;
|
||||
|
||||
return (
|
||||
<div className='commands'>
|
||||
@@ -35,6 +38,15 @@ export class Commands extends React.Component<CommandsProps, {}> {
|
||||
<VersionChooser appState={appState} />
|
||||
<Runner appState={appState} />
|
||||
</ControlGroup>
|
||||
{
|
||||
// tslint:disable-next-line jsx-no-multiline-js
|
||||
isBisectCommandShowing &&
|
||||
(
|
||||
<ControlGroup fill={true} vertical={false}>
|
||||
<BisectHandler appState={appState} />
|
||||
</ControlGroup>
|
||||
)
|
||||
}
|
||||
<ControlGroup fill={true} vertical={false}>
|
||||
<Button
|
||||
active={appState.isConsoleShowing}
|
||||
@@ -42,6 +54,7 @@ export class Commands extends React.Component<CommandsProps, {}> {
|
||||
text='Console'
|
||||
onClick={appState.toggleConsole}
|
||||
/>
|
||||
<EditorDropdown appState={appState} />
|
||||
</ControlGroup>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Button, Dialog, FileInput } from '@blueprintjs/core';
|
||||
import { shell } from 'electron';
|
||||
import * as fsType from 'fs-extra';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import { getTheme, THEMES_PATH } from '../themes';
|
||||
|
||||
import { GenericDialogType } from '../../../src/interfaces';
|
||||
import { AppState } from '../state';
|
||||
import { defaultDark, LoadedFiddleTheme } from '../themes-defaults';
|
||||
|
||||
export interface AddThemeDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface AddThemeDialogState {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "add monaco theme" dialog allows users to add custom editor themes.
|
||||
*
|
||||
* @class AddThemeDialog
|
||||
* @extends {React.Component<AddThemeDialogProps, AddThemeDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class AddThemeDialog extends React.Component<AddThemeDialogProps, AddThemeDialogState> {
|
||||
public resetState = { file: undefined };
|
||||
|
||||
constructor(props: AddThemeDialogProps) {
|
||||
super(props);
|
||||
this.state = this.resetState;
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onChangeFile = this.onChangeFile.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change of the file input
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
|
||||
const { files } = event.target as any;
|
||||
const file = files && files[0] ? files[0] : undefined;
|
||||
|
||||
this.setState({ file });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of the dialog
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async onSubmit(): Promise<void> {
|
||||
const { file } = this.state;
|
||||
const defaultTheme = !!this.props.appState.theme ? await getTheme(this.props.appState.theme) : defaultDark;
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const editor = fsType.readJSONSync(file.path);
|
||||
if (!editor.base && !editor.rules) throw Error('File does not match specifications'); // has to have these attributes
|
||||
defaultTheme.editor = editor as Partial<MonacoType.editor.IStandaloneThemeData>;
|
||||
const newTheme = defaultTheme;
|
||||
const name = editor.name ? editor.name : file.name;
|
||||
await this.createNewThemeFromMonaco(name, newTheme);
|
||||
} catch (error) {
|
||||
this.props.appState.setGenericDialogOptions({
|
||||
type: GenericDialogType.warning,
|
||||
label: `Error: ${error}, please pick a different file.`
|
||||
});
|
||||
this.props.appState.isGenericDialogShowing = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
public async createNewThemeFromMonaco(name: string, newTheme: LoadedFiddleTheme): Promise<boolean> {
|
||||
if (!name) return false;
|
||||
const themePath = path.join(THEMES_PATH, `${name}`);
|
||||
|
||||
await fsType.outputJSON(themePath, {
|
||||
...newTheme,
|
||||
name,
|
||||
}, {spaces: 2});
|
||||
|
||||
this.props.appState.setTheme(themePath);
|
||||
shell.showItemInFolder(themePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
const canSubmit = !!this.state.file;
|
||||
|
||||
return [
|
||||
(
|
||||
<Button
|
||||
icon='add'
|
||||
key='submit'
|
||||
disabled={!canSubmit}
|
||||
onClick={this.onSubmit}
|
||||
text='Add'
|
||||
/>
|
||||
), (
|
||||
<Button
|
||||
icon='cross'
|
||||
key='cancel'
|
||||
onClick={this.onClose}
|
||||
text='Cancel'
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
public onClose() {
|
||||
this.props.appState.isThemeDialogShowing = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isThemeDialogShowing } = this.props.appState;
|
||||
const inputProps = { accept: '.json' };
|
||||
const { file } = this.state;
|
||||
|
||||
const text = file && file.path ? file.path : `Select the Monaco file...`;
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isThemeDialogShowing}
|
||||
onClose={this.onClose}
|
||||
title='Add theme'
|
||||
className='dialog-add-version'
|
||||
>
|
||||
<div className='bp3-dialog-body'>
|
||||
<FileInput
|
||||
onInputChange={this.onChangeFile}
|
||||
inputProps={inputProps as any}
|
||||
text={text}
|
||||
/>
|
||||
<br />
|
||||
</div>
|
||||
<div className='bp3-dialog-footer'>
|
||||
<div className='bp3-dialog-footer-actions'>
|
||||
{this.buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this component's state
|
||||
*/
|
||||
private reset(): void {
|
||||
this.setState(this.resetState);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,8 +5,11 @@ import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { NpmVersion } from '../../interfaces';
|
||||
import { Version } from '../../interfaces';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { getElectronNameForPlatform } from '../../utils/electron-name';
|
||||
import { getIsDownloaded } from '../binary';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface AddVersionDialogProps {
|
||||
@@ -16,7 +19,7 @@ export interface AddVersionDialogProps {
|
||||
export interface AddVersionDialogState {
|
||||
isValidElectron: boolean;
|
||||
isValidVersion: boolean;
|
||||
file?: File;
|
||||
folderPath?: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
@@ -39,8 +42,15 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onChangeFile = this.onChangeFile.bind(this);
|
||||
this.onChangeVersion = this.onChangeVersion.bind(this);
|
||||
|
||||
ipcRendererManager.on(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, (_event, [file]) => {
|
||||
this.setFolderPath(file);
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.LOAD_LOCAL_VERSION_FOLDER);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,16 +58,10 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
|
||||
const { files } = event.target as any;
|
||||
const { binaryManager } = this.props.appState;
|
||||
const file = files && files[0] ? files[0] : undefined;
|
||||
public async setFolderPath(folderPath: string) {
|
||||
const isValidElectron = !!await getIsDownloaded('custom', folderPath);
|
||||
|
||||
const isValidElectron = !!(file
|
||||
&& file.path
|
||||
&& await binaryManager.getIsDownloaded('custom', file.path));
|
||||
|
||||
this.setState({ file, isValidElectron });
|
||||
this.setState({ folderPath, isValidElectron });
|
||||
}
|
||||
|
||||
public onChangeVersion(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
@@ -67,7 +71,7 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
this.setState({
|
||||
version,
|
||||
isValidVersion,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,18 +80,18 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async onSubmit(): Promise<void> {
|
||||
const { file, version } = this.state;
|
||||
const { folderPath, version } = this.state;
|
||||
|
||||
if (!file) return;
|
||||
if (!folderPath) return;
|
||||
|
||||
const name = file.path
|
||||
const name = folderPath
|
||||
.slice(-20)
|
||||
.split(path.sep)
|
||||
.slice(1)
|
||||
.join(path.sep);
|
||||
|
||||
const toAdd: NpmVersion = {
|
||||
localPath: file.path,
|
||||
const toAdd: Version = {
|
||||
localPath: folderPath,
|
||||
version,
|
||||
name
|
||||
};
|
||||
@@ -104,18 +108,6 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this component's state
|
||||
*/
|
||||
public reset(): void {
|
||||
this.setState({
|
||||
isValidElectron: false,
|
||||
isValidVersion: false,
|
||||
version: '',
|
||||
file: undefined
|
||||
});
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
const canSubmit = this.state.isValidElectron && this.state.isValidVersion;
|
||||
|
||||
@@ -141,12 +133,16 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
|
||||
public render() {
|
||||
const { isAddVersionDialogShowing } = this.props.appState;
|
||||
const inputProps = { webkitdirectory: 'true' };
|
||||
const { file } = this.state;
|
||||
const inputProps = {
|
||||
onClick: (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
ipcRendererManager.send(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG);
|
||||
}
|
||||
};
|
||||
const { folderPath } = this.state;
|
||||
|
||||
const text = file && file.path
|
||||
? file.path
|
||||
: `Select the folder containing ${getElectronNameForPlatform()}...`;
|
||||
const text = folderPath ||
|
||||
`Select the folder containing ${getElectronNameForPlatform()}...`;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -157,7 +153,6 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
>
|
||||
<div className='bp3-dialog-body'>
|
||||
<FileInput
|
||||
onInputChange={this.onChangeFile}
|
||||
id='custom-electron-version'
|
||||
inputProps={inputProps as any}
|
||||
text={text}
|
||||
@@ -175,9 +170,9 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
}
|
||||
|
||||
private renderPath(): JSX.Element | null {
|
||||
const { file, isValidElectron } = this.state;
|
||||
const { folderPath, isValidElectron } = this.state;
|
||||
|
||||
if (!file || !file.path) return null;
|
||||
if (!folderPath) return null;
|
||||
|
||||
const info = isValidElectron
|
||||
? `We found an ${getElectronNameForPlatform()} in this folder.`
|
||||
@@ -212,4 +207,15 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this component's state
|
||||
*/
|
||||
private reset(): void {
|
||||
this.setState({
|
||||
isValidElectron: false,
|
||||
isValidVersion: false,
|
||||
version: '',
|
||||
folderPath: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { Button, ButtonGroup, Callout, Dialog, Label } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { RunnableVersion } from '../../interfaces';
|
||||
import { Bisector } from '../bisect';
|
||||
import { AppState } from '../state';
|
||||
import { VersionSelect } from './version-select';
|
||||
|
||||
export interface BisectDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface BisectDialogState {
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
allVersions: Array<RunnableVersion>;
|
||||
showHelp?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "add version" dialog allows users to add custom builds of Electron.
|
||||
*
|
||||
* @class BisectDialog
|
||||
* @extends {React.Component<BisectDialogProps, BisectDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class BisectDialog extends React.Component<BisectDialogProps, BisectDialogState> {
|
||||
constructor(props: BisectDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onBeginSelect = this.onBeginSelect.bind(this);
|
||||
this.onEndSelect = this.onEndSelect.bind(this);
|
||||
this.showHelp = this.showHelp.bind(this);
|
||||
this.isEarliestItemDisabled = this.isEarliestItemDisabled.bind(this);
|
||||
this.isLatestItemDisabled = this.isLatestItemDisabled.bind(this);
|
||||
|
||||
this.state = {
|
||||
allVersions: this.props.appState.versionsToShow,
|
||||
startIndex: 10,
|
||||
endIndex: 0
|
||||
};
|
||||
}
|
||||
|
||||
public onBeginSelect(version: RunnableVersion) {
|
||||
this.setState({ startIndex: this.state.allVersions.indexOf(version) });
|
||||
}
|
||||
|
||||
public onEndSelect(version: RunnableVersion) {
|
||||
this.setState({ endIndex: this.state.allVersions.indexOf(version) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of the dialog
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async onSubmit(): Promise<void> {
|
||||
const { endIndex, startIndex, allVersions } = this.state;
|
||||
const { appState } = this.props;
|
||||
|
||||
if (endIndex === undefined || startIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bisectRange = allVersions
|
||||
.slice(endIndex, startIndex + 1)
|
||||
.reverse();
|
||||
|
||||
appState.Bisector = new Bisector(bisectRange);
|
||||
const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
|
||||
appState.setVersion(initialBisectPivot);
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog
|
||||
*/
|
||||
public onClose() {
|
||||
this.props.appState.isBisectDialogShowing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the additional help
|
||||
*/
|
||||
public showHelp() {
|
||||
this.setState({ showHelp: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Can we get this show on the road?
|
||||
*/
|
||||
get canSubmit(): boolean {
|
||||
return this.state.startIndex > this.state.endIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the buttons
|
||||
*/
|
||||
get buttons() {
|
||||
return [
|
||||
(
|
||||
<Button
|
||||
icon='play'
|
||||
key='submit'
|
||||
disabled={!this.canSubmit}
|
||||
onClick={this.onSubmit}
|
||||
text='Begin'
|
||||
/>
|
||||
), (
|
||||
<Button
|
||||
icon='cross'
|
||||
key='cancel'
|
||||
onClick={this.onClose}
|
||||
text='Cancel'
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the help
|
||||
*/
|
||||
get help() {
|
||||
let moreHelp = (
|
||||
<Button
|
||||
icon='help'
|
||||
text='Show help'
|
||||
onClick={this.showHelp}
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.state.showHelp) {
|
||||
moreHelp = (
|
||||
<>
|
||||
<p>
|
||||
First, write a fiddle that reproduces a bug or an issue. Then, select the earliest version to
|
||||
start your search with. Typically, that's the "last known good" version that did not have the bug.
|
||||
Then, select that latest version to end the search with, usually the "first known bad" version.
|
||||
</p>
|
||||
<p>
|
||||
Once you begin your bisect, Fiddle will run your fiddle with a number of Electron versions, closing
|
||||
in on the version that introduced the bug. Once completed, you will know which Electron version
|
||||
introduced your issue.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Callout style={{ marginTop: 0, marginBottom: '1rem' }}>
|
||||
<p>
|
||||
A "bisect" is a popular method <a href='https://git-scm.com/docs/git-bisect' target='_blank'>
|
||||
borrowed from <code>git</code></a> for learning which version of Electron introduced a bug. This
|
||||
tool helps you perform a bisect.
|
||||
</p>
|
||||
{moreHelp}
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isBisectDialogShowing } = this.props.appState;
|
||||
const { startIndex, endIndex, allVersions } = this.state;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isBisectDialogShowing}
|
||||
onClose={this.onClose}
|
||||
title='Start a bisect session'
|
||||
className='dialog-add-version'
|
||||
>
|
||||
<div className='bp3-dialog-body'>
|
||||
{this.help}
|
||||
<Label>
|
||||
Earliest Version (Last "known good" version)
|
||||
<ButtonGroup fill={true}>
|
||||
<VersionSelect
|
||||
currentVersion={allVersions[startIndex]}
|
||||
appState={this.props.appState}
|
||||
onVersionSelect={this.onBeginSelect}
|
||||
itemDisabled={this.isEarliestItemDisabled}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Label>
|
||||
<Label>
|
||||
Latest Version (First "known bad" version)
|
||||
<ButtonGroup fill={true}>
|
||||
<VersionSelect
|
||||
currentVersion={allVersions[endIndex]}
|
||||
appState={this.props.appState}
|
||||
onVersionSelect={this.onEndSelect}
|
||||
itemDisabled={this.isLatestItemDisabled}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Label>
|
||||
</div>
|
||||
<div className='bp3-dialog-footer'>
|
||||
<div className='bp3-dialog-footer-actions'>
|
||||
{this.buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should an item in the "earliest version" dropdown be disabled?
|
||||
*
|
||||
* @param {RunnableVersion} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isEarliestItemDisabled(version: RunnableVersion): boolean {
|
||||
const { allVersions, endIndex } = this.state;
|
||||
|
||||
// In the array, "newer" versions will have a lower index.
|
||||
// 0: 5.0.0
|
||||
// 1: 4.0.0
|
||||
// 2: 3.0.0
|
||||
// ...
|
||||
return allVersions.indexOf(version) < endIndex + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should an item in the "latest version" dropdown be disabled?
|
||||
*
|
||||
* @param {RunnableVersion} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isLatestItemDisabled(version: RunnableVersion): boolean {
|
||||
const { allVersions, startIndex } = this.state;
|
||||
|
||||
return allVersions.indexOf(version) > startIndex - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
import { Alert, IconName, Intent} from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GenericDialogType } from '../../../src/interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface GenericDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface GenericDialogState {
|
||||
}
|
||||
|
||||
/**
|
||||
* The token dialog prompts the user to either continue or cancel the operation.
|
||||
*
|
||||
* @export
|
||||
* @class GenericDialog
|
||||
* @extends {React.Component<GenericDialogProps, GenericDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class GenericDialog extends React.Component<GenericDialogProps, GenericDialogState> {
|
||||
constructor(props: GenericDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
public onClose(result: boolean) {
|
||||
this.props.appState.genericDialogLastResult = result;
|
||||
this.props.appState.toggleGenericDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isGenericDialogShowing, genericDialogOptions } = this.props.appState;
|
||||
const {type, ok, cancel, label} = genericDialogOptions;
|
||||
|
||||
let intent: Intent;
|
||||
let icon: IconName;
|
||||
switch (type) {
|
||||
case GenericDialogType.warning:
|
||||
intent = Intent.DANGER;
|
||||
icon = 'warning-sign';
|
||||
break;
|
||||
case GenericDialogType.confirm:
|
||||
intent = Intent.PRIMARY;
|
||||
icon = 'help';
|
||||
break;
|
||||
case GenericDialogType.success:
|
||||
intent = Intent.SUCCESS;
|
||||
icon = 'info-sign';
|
||||
break;
|
||||
default:
|
||||
intent = Intent.NONE;
|
||||
icon = 'help';
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<Alert
|
||||
isOpen={isGenericDialogShowing}
|
||||
onClose={this.onClose}
|
||||
icon={icon}
|
||||
confirmButtonText={ok}
|
||||
cancelButtonText={cancel}
|
||||
intent={intent}
|
||||
>
|
||||
<p>{label}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export interface TokenDialogProps {
|
||||
}
|
||||
|
||||
export interface TokenDialogState {
|
||||
tokenInput?: string;
|
||||
tokenInput: string;
|
||||
verifying: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
@@ -37,7 +37,9 @@ export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogSt
|
||||
this.state = {
|
||||
verifying: false,
|
||||
error: false,
|
||||
tokenInput: ''
|
||||
};
|
||||
|
||||
this.onSubmitToken = this.onSubmitToken.bind(this);
|
||||
this.openGenerateTokenExternal = this.openGenerateTokenExternal.bind(this);
|
||||
this.onTokenInputFocused = this.onTokenInputFocused.bind(this);
|
||||
@@ -53,31 +55,25 @@ export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogSt
|
||||
*/
|
||||
public async onSubmitToken(): Promise<void> {
|
||||
if (!this.state.tokenInput) return;
|
||||
this.setState({
|
||||
verifying: true,
|
||||
});
|
||||
this.setState({ verifying: true });
|
||||
this.props.appState.gitHubToken = this.state.tokenInput;
|
||||
|
||||
const octo = await getOctokit();
|
||||
octo.authenticate({
|
||||
type: 'token',
|
||||
token: this.state.tokenInput || '',
|
||||
});
|
||||
const octo = await getOctokit(this.props.appState);
|
||||
|
||||
try {
|
||||
const { data } = await octo.users.get({});
|
||||
const { data } = await octo.users.getAuthenticated();
|
||||
this.props.appState.gitHubAvatarUrl = data.avatar_url;
|
||||
this.props.appState.gitHubLogin = data.login;
|
||||
this.props.appState.gitHubName = data.name;
|
||||
this.setState({ verifying: false, error: false });
|
||||
} catch (error) {
|
||||
console.warn(`Authenticating against GitHub failed`, error);
|
||||
this.setState({ verifying: false, error: true });
|
||||
this.props.appState.gitHubToken = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.appState.gitHubToken = this.state.tokenInput;
|
||||
this.props.appState.isTokenDialogShowing = false;
|
||||
|
||||
this.setState({ verifying: false, error: false });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface WarningDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface WarningDialogState {
|
||||
}
|
||||
|
||||
/**
|
||||
* The token dialog asks the user for a GitHub Personal Access Token.
|
||||
* It's also responsible for checking if the token is correct.
|
||||
*
|
||||
* @export
|
||||
* @class WarningDialog
|
||||
* @extends {React.Component<WarningDialogProps, WarningDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class WarningDialog extends React.Component<WarningDialogProps, WarningDialogState> {
|
||||
constructor(props: WarningDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
public onClose(result: boolean) {
|
||||
this.props.appState.warningDialogLastResult = result;
|
||||
this.props.appState.toogleWarningDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isWarningDialogShowing, warningDialogTexts } = this.props.appState;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
isOpen={isWarningDialogShowing}
|
||||
onClose={this.onClose}
|
||||
icon='warning-sign'
|
||||
confirmButtonText={warningDialogTexts.ok}
|
||||
cancelButtonText={warningDialogTexts.cancel}
|
||||
intent={Intent.DANGER}
|
||||
>
|
||||
<p>{warningDialogTexts.label}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ArrowPosition } from '../../interfaces';
|
||||
import { classNames } from '../../utils/classnames';
|
||||
import { sortButtons } from '../../utils/sort-buttons';
|
||||
|
||||
export interface DialogProps {
|
||||
buttons?: Array<JSX.Element> | null;
|
||||
style?: React.CSSProperties;
|
||||
isCentered?: boolean;
|
||||
className?: string;
|
||||
isShowing?: boolean;
|
||||
isShowingBackdrop?: boolean;
|
||||
arrow?: ArrowPosition;
|
||||
onClose?: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic dialog class for Fiddle.
|
||||
*
|
||||
* @class Dialog
|
||||
* @extends {React.Component<DialogProps, {}>}
|
||||
*/
|
||||
export class Dialog extends React.Component<DialogProps, {}> {
|
||||
public static defaultProps: Partial<DialogProps> = {
|
||||
isCentered: false,
|
||||
isShowing: true,
|
||||
isShowingBackdrop: false
|
||||
};
|
||||
|
||||
constructor(props: DialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onConfirm = this.onConfirm.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog
|
||||
*/
|
||||
public onClose() {
|
||||
if (this.props.onClose) this.props.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the dialog
|
||||
*/
|
||||
public onConfirm() {
|
||||
if (this.props.onConfirm) this.props.onConfirm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the dialog's buttons, using a default "Ok"
|
||||
* if none were passed.
|
||||
*
|
||||
* @returns {Array<JSX.Element> | JSX.Element}
|
||||
*/
|
||||
public renderButtons(): Array<JSX.Element | null> | JSX.Element {
|
||||
const { buttons, onClose } = this.props;
|
||||
const closeButton = onClose
|
||||
? <button key='cancel' className='btn-close' onClick={this.onClose}>Cancel</button>
|
||||
: null;
|
||||
const okButton = (
|
||||
<button key='ok' className='btn-ok' onClick={this.onConfirm}>Ok</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='dialog-buttons'>
|
||||
{sortButtons(buttons! || [ closeButton, okButton ])}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally renders a backdrop that closes the
|
||||
* dialog if clicked.
|
||||
*
|
||||
* @returns {JSX.Element | null}
|
||||
*/
|
||||
public renderBackdrop(): JSX.Element | null {
|
||||
const { isShowingBackdrop } = this.props;
|
||||
|
||||
if (!isShowingBackdrop) return null;
|
||||
|
||||
return (
|
||||
<div key='drop' className='dialogDrop' onClick={this.onClose} />
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isShowing, isCentered, style, children, className, arrow } = this.props;
|
||||
const arrowClass = arrow ? `arrow arrow-${arrow}` : '';
|
||||
const parsedClassName = classNames(
|
||||
'dialog',
|
||||
arrowClass,
|
||||
{ centered: isCentered },
|
||||
className
|
||||
);
|
||||
|
||||
return isShowing ? (
|
||||
<>
|
||||
{this.renderBackdrop()}
|
||||
<div style={style} className={parsedClassName}>
|
||||
{children}
|
||||
{this.renderButtons()}
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { AddThemeDialog } from './dialog-add-theme';
|
||||
import { AddVersionDialog } from './dialog-add-version';
|
||||
import { BisectDialog } from './dialog-bisect';
|
||||
import { GenericDialog } from './dialog-generic';
|
||||
import { TokenDialog } from './dialog-token';
|
||||
import { WarningDialog } from './dialog-warning';
|
||||
import { Settings } from './settings';
|
||||
|
||||
export interface DialogsProps {
|
||||
@@ -21,7 +23,14 @@ export interface DialogsProps {
|
||||
export class Dialogs extends React.Component<DialogsProps, {}> {
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { isTokenDialogShowing, isSettingsShowing, isAddVersionDialogShowing } = appState;
|
||||
const {
|
||||
isTokenDialogShowing,
|
||||
isSettingsShowing,
|
||||
isAddVersionDialogShowing,
|
||||
isThemeDialogShowing,
|
||||
isBisectDialogShowing,
|
||||
isGenericDialogShowing
|
||||
} = appState;
|
||||
const maybeToken = isTokenDialogShowing
|
||||
? <TokenDialog key='dialogs' appState={appState} />
|
||||
: null;
|
||||
@@ -31,13 +40,23 @@ export class Dialogs extends React.Component<DialogsProps, {}> {
|
||||
const maybeAddLocalVersion = isAddVersionDialogShowing
|
||||
? <AddVersionDialog key='add-version-dialog' appState={appState} />
|
||||
: null;
|
||||
const maybeMonaco = isThemeDialogShowing ? <AddThemeDialog appState={appState} />
|
||||
: null;
|
||||
const maybeBisect = isBisectDialogShowing
|
||||
? <BisectDialog key='bisect-dialog' appState={appState} />
|
||||
: null;
|
||||
const genericDialog = isGenericDialogShowing
|
||||
? <GenericDialog appState={appState} />
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div key='dialogs' className='dialogs'>
|
||||
{maybeToken}
|
||||
{maybeSettings}
|
||||
{maybeAddLocalVersion}
|
||||
<WarningDialog appState={appState} />
|
||||
{maybeMonaco}
|
||||
{maybeBisect}
|
||||
{genericDialog}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ContentNames, getContent } from '../content';
|
||||
import { EditorId } from '../../interfaces';
|
||||
import { getContent } from '../content';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface EditorProps {
|
||||
appState: AppState;
|
||||
monaco: typeof MonacoType;
|
||||
monoacoOptions: MonacoType.editor.IEditorOptions;
|
||||
id: string;
|
||||
monacoOptions: MonacoType.editor.IEditorOptions;
|
||||
id: EditorId;
|
||||
options?: Partial<MonacoType.editor.IEditorConstructionOptions>;
|
||||
editorDidMount?: (editor: MonacoType.editor.IStandaloneCodeEditor) => void;
|
||||
onChange?: (value: string, event: MonacoType.editor.IModelContentChangedEvent) => void;
|
||||
setFocused: (id: EditorId) => void;
|
||||
}
|
||||
|
||||
export class Editor extends React.Component<EditorProps> {
|
||||
@@ -27,15 +29,24 @@ export class Editor extends React.Component<EditorProps> {
|
||||
constructor(props: EditorProps) {
|
||||
super(props);
|
||||
|
||||
this.language = props.id === 'html' ? 'html' : 'javascript';
|
||||
switch (props.id) {
|
||||
case 'html':
|
||||
this.language = 'html';
|
||||
break;
|
||||
case 'css':
|
||||
this.language = 'css';
|
||||
break;
|
||||
default:
|
||||
this.language = 'javascript';
|
||||
}
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.initMonaco();
|
||||
public async componentDidMount() {
|
||||
await this.initMonaco();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
@@ -48,11 +59,16 @@ export class Editor extends React.Component<EditorProps> {
|
||||
*
|
||||
* @param {MonacoType.editor.IStandaloneCodeEditor} editor
|
||||
*/
|
||||
public editorDidMount(editor: MonacoType.editor.IStandaloneCodeEditor) {
|
||||
public async editorDidMount(editor: MonacoType.editor.IStandaloneCodeEditor) {
|
||||
const { editorDidMount } = this.props;
|
||||
|
||||
// Set the content on the editor
|
||||
await this.setContent();
|
||||
|
||||
// Set the editor as an available object
|
||||
window.ElectronFiddle.editors[this.props.id] = editor;
|
||||
|
||||
// And notify others
|
||||
if (editorDidMount) {
|
||||
editorDidMount(editor);
|
||||
}
|
||||
@@ -62,8 +78,7 @@ export class Editor extends React.Component<EditorProps> {
|
||||
* Initialize Monaco.
|
||||
*/
|
||||
public async initMonaco() {
|
||||
const { monaco, id, appState, monoacoOptions } = this.props;
|
||||
const { version } = appState;
|
||||
const { monaco, monacoOptions: monacoOptions } = this.props;
|
||||
const ref = this.containerRef.current;
|
||||
|
||||
if (ref) {
|
||||
@@ -71,10 +86,17 @@ export class Editor extends React.Component<EditorProps> {
|
||||
language: this.language,
|
||||
theme: 'main',
|
||||
contextmenu: false,
|
||||
value: await getContent(id as ContentNames, version),
|
||||
...monoacoOptions
|
||||
model: null,
|
||||
...monacoOptions
|
||||
});
|
||||
this.editorDidMount(this.editor);
|
||||
|
||||
// mark this editor as focused whenever it is
|
||||
this.editor.onDidFocusEditorText(() => {
|
||||
const { id, setFocused } = this.props;
|
||||
setFocused(id);
|
||||
});
|
||||
|
||||
await this.editorDidMount(this.editor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +105,7 @@ export class Editor extends React.Component<EditorProps> {
|
||||
*/
|
||||
public destroyMonaco() {
|
||||
if (typeof this.editor !== 'undefined') {
|
||||
console.log('Editor: Disposing');
|
||||
this.editor.dispose();
|
||||
}
|
||||
}
|
||||
@@ -90,4 +113,52 @@ export class Editor extends React.Component<EditorProps> {
|
||||
public render() {
|
||||
return <div className='editorContainer' ref={this.containerRef} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model and attach it to the editor
|
||||
*
|
||||
* @private
|
||||
* @param {string} value
|
||||
*/
|
||||
private createModel(value: string) {
|
||||
const { monaco } = this.props;
|
||||
|
||||
const model = monaco.editor.createModel(value, this.language);
|
||||
model.updateOptions({
|
||||
tabSize: 2
|
||||
});
|
||||
|
||||
this.editor.setModel(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content on the editor, including the model and the view state.
|
||||
*
|
||||
* @private
|
||||
* @memberof Editor
|
||||
*/
|
||||
private async setContent() {
|
||||
const { appState, id } = this.props;
|
||||
const { version } = appState;
|
||||
|
||||
const backup = appState.getAndRemoveEditorValueBackup(id);
|
||||
|
||||
if (backup) {
|
||||
console.log(`Editor: Backup found, restoring state`);
|
||||
|
||||
if (backup.viewState) {
|
||||
this.editor.restoreViewState(backup.viewState);
|
||||
}
|
||||
|
||||
// If there's a model, use the model. No model? Use the value
|
||||
if (backup.model) {
|
||||
this.editor.setModel(backup.model);
|
||||
} else {
|
||||
this.createModel(backup.value ?? '');
|
||||
}
|
||||
} else {
|
||||
const value = await getContent(id, version);
|
||||
this.createModel(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, NonIdealState } from '@blueprintjs/core';
|
||||
|
||||
import { EditorId } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export function renderNonIdealState(appState: AppState) {
|
||||
const allEditors = [ EditorId.html, EditorId.main, EditorId.renderer ];
|
||||
const resolveButton = (
|
||||
<Button
|
||||
text='Open all editors'
|
||||
onClick={() => appState.setVisibleMosaics(allEditors)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NonIdealState
|
||||
action={resolveButton}
|
||||
icon='applications'
|
||||
description='You have closed all editors. You can open them again with the button below or the "Editors" button above!'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { MosaicContext, MosaicRootActions, MosaicWindowContext } from 'react-mosaic-component';
|
||||
|
||||
import { DocsDemoPage, MosaicId } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ToolbarButtonProps {
|
||||
appState: AppState;
|
||||
id: MosaicId;
|
||||
}
|
||||
|
||||
export abstract class ToolbarButton extends React.PureComponent<ToolbarButtonProps> {
|
||||
public static contextType = MosaicWindowContext;
|
||||
public context: MosaicWindowContext;
|
||||
|
||||
constructor(props: ToolbarButtonProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<MosaicContext.Consumer>
|
||||
{({ mosaicActions }) => this.createButton(mosaicActions)}
|
||||
</MosaicContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a button that performs the actual action
|
||||
*/
|
||||
public abstract createButton(_mosaicActions: MosaicRootActions<any>): React.ReactNode;
|
||||
}
|
||||
|
||||
export class MaximizeButton extends ToolbarButton {
|
||||
/**
|
||||
* Create a button that can expand this panel
|
||||
*/
|
||||
public createButton(mosaicActions: MosaicRootActions<any>) {
|
||||
const onClick = () => {
|
||||
mosaicActions.expand(this.context.mosaicWindowActions.getPath());
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon='maximize'
|
||||
className='bp3-small'
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveButton extends ToolbarButton {
|
||||
/**
|
||||
* Create a button that can remove this panel
|
||||
*/
|
||||
public createButton(_mosaicActions: MosaicRootActions<any>) {
|
||||
const onClick = () => this.props.appState.hideAndBackupMosaic(this.props.id);
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon='cross'
|
||||
className='bp3-small'
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class DocsDemoGoHomeButton extends ToolbarButton {
|
||||
/**
|
||||
* Create a button that can remove this panel
|
||||
*/
|
||||
public createButton(_mosaicActions: MosaicRootActions<any>) {
|
||||
const onClick = () => this.props.appState.currentDocsDemoPage = DocsDemoPage.DEFAULT;
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon='home'
|
||||
className='bp3-small'
|
||||
onClick={onClick}
|
||||
text='Overview'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
import { reaction } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as React from 'react';
|
||||
import { Mosaic, MosaicWindow } from 'react-mosaic-component';
|
||||
import { Mosaic, MosaicBranch, MosaicNode, MosaicWindow, MosaicWindowProps } from 'react-mosaic-component';
|
||||
|
||||
import { EditorId } from '../../interfaces';
|
||||
import { EditorId, MosaicId, PanelId } from '../../interfaces';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { updateEditorLayout } from '../../utils/editor-layout';
|
||||
import { getFocusedEditor } from '../../utils/focused-editor';
|
||||
import { getAtPath, setAtPath } from '../../utils/js-path';
|
||||
import { toggleMonaco } from '../../utils/toggle-monaco';
|
||||
import { ContentNames, getContent } from '../content';
|
||||
import { isEditorId } from '../../utils/type-checks';
|
||||
import { getContent } from '../content';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
import { activateTheme } from '../themes';
|
||||
import { Editor } from './editor';
|
||||
|
||||
const options: any = {
|
||||
direction: 'row',
|
||||
first: 'main',
|
||||
second: {
|
||||
direction: 'column',
|
||||
first: 'renderer',
|
||||
second: 'html',
|
||||
},
|
||||
};
|
||||
import { renderNonIdealState } from './editors-non-ideal-state';
|
||||
import { DocsDemoGoHomeButton, MaximizeButton, RemoveButton } from './editors-toolbar-button';
|
||||
import { ShowMe } from './show-me';
|
||||
|
||||
const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
|
||||
minimap: {
|
||||
@@ -31,13 +27,13 @@ const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
|
||||
wordWrap: 'on'
|
||||
};
|
||||
|
||||
const ViewIdMosaic = Mosaic.ofType<EditorId>();
|
||||
const ViewIdMosaicWindow = MosaicWindow.ofType<EditorId>();
|
||||
|
||||
const TITLE_MAP: Record<EditorId, string> = {
|
||||
main: 'Main Process',
|
||||
renderer: 'Renderer Process',
|
||||
html: 'HTML'
|
||||
export const TITLE_MAP: Record<MosaicId, string> = {
|
||||
main: 'Main Process (main.js)',
|
||||
renderer: 'Renderer Process (renderer.js)',
|
||||
preload: 'Preload (preload.js)',
|
||||
html: 'HTML (index.html)',
|
||||
css: 'Stylesheet (styles.css)',
|
||||
docsDemo: 'Docs & Demos',
|
||||
};
|
||||
|
||||
export interface EditorsProps {
|
||||
@@ -48,16 +44,36 @@ export interface EditorsState {
|
||||
monaco?: typeof MonacoType;
|
||||
isMounted?: boolean;
|
||||
monacoOptions: MonacoType.editor.IEditorOptions;
|
||||
focused?: EditorId;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
// A reaction: Each time mosaicArrangement is changed, we'll update
|
||||
// the editor layout. That method is itself debounced.
|
||||
public disposeLayoutAutorun = reaction(
|
||||
() => this.props.appState.mosaicArrangement,
|
||||
() => updateEditorLayout()
|
||||
);
|
||||
|
||||
constructor(props: EditorsProps) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.renderEditor = this.renderEditor.bind(this);
|
||||
this.renderTile = this.renderTile.bind(this);
|
||||
this.renderGenericPanel = this.renderGenericPanel.bind(this);
|
||||
this.setFocused = this.setFocused.bind(this);
|
||||
|
||||
this.state = { monacoOptions: defaultMonacoOptions };
|
||||
this.loadMonaco();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
/**
|
||||
* Executed right after the component mounts. We'll setup the IPC listeners here.
|
||||
*
|
||||
* @memberof Editors
|
||||
*/
|
||||
public async componentDidMount() {
|
||||
ipcRendererManager.on(IpcEvents.MONACO_EXECUTE_COMMAND, (_event, cmd: string) => {
|
||||
this.executeCommand(cmd);
|
||||
});
|
||||
@@ -65,26 +81,28 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
ipcRendererManager.on(IpcEvents.FS_NEW_FIDDLE, async (_event) => {
|
||||
const { version } = this.props.appState;
|
||||
|
||||
this.props.appState.setWarningDialogTexts({
|
||||
label: 'Your current fiddle is unsaved. Do you want to discard it?'
|
||||
});
|
||||
|
||||
window.ElectronFiddle.app.setValues({
|
||||
html: await getContent(ContentNames.HTML, version),
|
||||
renderer: await getContent(ContentNames.RENDERER, version),
|
||||
main: await getContent(ContentNames.MAIN, version),
|
||||
});
|
||||
await window.ElectronFiddle.app.replaceFiddle({
|
||||
html: await getContent(EditorId.html, version),
|
||||
renderer: await getContent(EditorId.renderer, version),
|
||||
main: await getContent(EditorId.main, version),
|
||||
}, {});
|
||||
});
|
||||
|
||||
ipcRendererManager.on(IpcEvents.TOGGLE_SOFT_WRAP, async (_event) => {
|
||||
this.toggleEditorOption('wordWrap');
|
||||
});
|
||||
|
||||
ipcRendererManager.on(IpcEvents.TOGGLE_MINI_MAP, async (_event) => {
|
||||
this.toggleEditorOption('minimap.enabled');
|
||||
ipcRendererManager.on(IpcEvents.MONACO_TOGGLE_OPTION, (_event, cmd: string) => {
|
||||
this.toggleEditorOption(cmd);
|
||||
});
|
||||
|
||||
this.setState({ isMounted: true });
|
||||
await this.loadMonaco();
|
||||
this.props.appState.isUnsaved = false;
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.disposeLayoutAutorun();
|
||||
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_EXECUTE_COMMAND);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.FS_NEW_FIDDLE);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_TOGGLE_OPTION);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,9 +125,9 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
}
|
||||
}
|
||||
|
||||
public toggleEditorOption(path: string) {
|
||||
if (!window || !window.ElectronFiddle || !window.ElectronFiddle.editors) {
|
||||
return;
|
||||
public toggleEditorOption(path: string): boolean {
|
||||
if (!window.ElectronFiddle.editors) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -120,21 +138,128 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
setAtPath(path, newOptions, toggleMonaco(currentSetting));
|
||||
|
||||
Object.keys(window.ElectronFiddle.editors)
|
||||
.forEach((key) => {
|
||||
const editor: MonacoType.editor.IStandaloneCodeEditor | null
|
||||
= window.ElectronFiddle.editors[key];
|
||||
.forEach((key) => {
|
||||
const editor: MonacoType.editor.IStandaloneCodeEditor | null
|
||||
= window.ElectronFiddle.editors[key];
|
||||
|
||||
if (editor) {
|
||||
editor.updateOptions(newOptions);
|
||||
}
|
||||
});
|
||||
if (editor) {
|
||||
editor.updateOptions(newOptions);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ monacoOptions: newOptions });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Editors: Could not toggle property ${path}`, error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the little tool bar on top of each panel
|
||||
*
|
||||
* @param {MosaicWindowProps<MosaicId>} { title }
|
||||
* @param {MosaicId} id
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
public renderToolbar(
|
||||
{ title }: MosaicWindowProps<MosaicId>, id: MosaicId
|
||||
): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const docsDemoGoHomeMaybe = id === PanelId.docsDemo
|
||||
? <DocsDemoGoHomeButton id={id} appState={appState} />
|
||||
: null;
|
||||
|
||||
// only show toolbar controls if we have more than 1 visible editor
|
||||
// Mosaic arrangement is type string if 1 editor, object otherwise
|
||||
const toolbarControlsMaybe =
|
||||
(typeof appState.mosaicArrangement !== 'string') &&
|
||||
(
|
||||
<>
|
||||
<MaximizeButton id={id} appState={appState} />
|
||||
<RemoveButton id={id} appState={appState} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Left */}
|
||||
<div>
|
||||
<h5>
|
||||
{title}
|
||||
</h5>
|
||||
</div>
|
||||
{/* Middle */}
|
||||
<div />
|
||||
{/* Right */}
|
||||
<div className='mosaic-controls'>
|
||||
{docsDemoGoHomeMaybe}
|
||||
{toolbarControlsMaybe}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Mosaic tile
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {string} path
|
||||
* @returns {JSX.Element | null}
|
||||
*/
|
||||
public renderTile(id: MosaicId, path: Array<MosaicBranch>): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const content = isEditorId(id)
|
||||
? this.renderEditor(id)
|
||||
: this.renderGenericPanel(id, appState);
|
||||
|
||||
return (
|
||||
<MosaicWindow<EditorId>
|
||||
className={id}
|
||||
path={path}
|
||||
title={TITLE_MAP[id]}
|
||||
renderToolbar={(props: MosaicWindowProps<MosaicId>) => this.renderToolbar(props, id)}
|
||||
>
|
||||
{content}
|
||||
</MosaicWindow>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a generic panel – so not an editor, but something else
|
||||
*
|
||||
* @param {PanelId} _id
|
||||
* @param {AppState} _appState
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
public renderGenericPanel(_id: PanelId, appState: AppState): JSX.Element {
|
||||
return <ShowMe appState={appState} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an editor
|
||||
*
|
||||
* @param {EditorId} id
|
||||
* @returns {(JSX.Element | null)}
|
||||
* @memberof Editors
|
||||
*/
|
||||
public renderEditor(id: EditorId): JSX.Element | null {
|
||||
const { appState } = this.props;
|
||||
const { monaco } = this.state;
|
||||
|
||||
return (
|
||||
<Editor
|
||||
id={id}
|
||||
monaco={monaco!}
|
||||
appState={appState}
|
||||
monacoOptions={defaultMonacoOptions}
|
||||
setFocused={this.setFocused}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { monaco } = this.state;
|
||||
@@ -142,27 +267,31 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
if (!monaco) return null;
|
||||
|
||||
return (
|
||||
<ViewIdMosaic
|
||||
onChange={updateEditorLayout}
|
||||
// tslint:disable-next-line:jsx-no-multiline-js
|
||||
renderTile={(id, path) => (
|
||||
<ViewIdMosaicWindow className={id} path={path} title={TITLE_MAP[id]} toolbarControls={<div />}>
|
||||
<Editor id={id} monaco={monaco} appState={appState} monoacoOptions={defaultMonacoOptions} />
|
||||
</ViewIdMosaicWindow>
|
||||
)}
|
||||
initialValue={options}
|
||||
<Mosaic<EditorId | PanelId>
|
||||
className={`focused__${this.state.focused}`}
|
||||
onChange={this.onChange}
|
||||
value={appState.mosaicArrangement}
|
||||
zeroStateView={renderNonIdealState(appState)}
|
||||
renderTile={this.renderTile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in the visible nodes
|
||||
*
|
||||
* @param {(MosaicNode<EditorId> | null)} currentNode
|
||||
*/
|
||||
public onChange(currentNode: MosaicNode<EditorId> | null) {
|
||||
this.props.appState.mosaicArrangement = currentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads monaco. If it's already loaded, it'll just set it on the current state.
|
||||
* We're doing things a bit roundabout to ensure that we're not overloading the
|
||||
* mobx state with a gigantic Monaco tree.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private async loadMonaco(): Promise<void> {
|
||||
public async loadMonaco() {
|
||||
const { app } = window.ElectronFiddle;
|
||||
const loader = require('monaco-loader');
|
||||
const monaco = app.monaco || await loader();
|
||||
@@ -172,11 +301,24 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
}
|
||||
|
||||
if (!this.state || !this.state.isMounted) {
|
||||
this.state = { monaco, monacoOptions: defaultMonacoOptions };
|
||||
this.setState({
|
||||
monaco,
|
||||
monacoOptions: defaultMonacoOptions
|
||||
});
|
||||
} else {
|
||||
this.setState({ monaco });
|
||||
}
|
||||
|
||||
activateTheme(monaco, undefined, this.props.appState.theme);
|
||||
await activateTheme(monaco, undefined, this.props.appState.theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currently-focused editor. This will impact the editor's
|
||||
* z-index, ensuring that its intellisense menus don't get clipped
|
||||
* by the other editor windows.
|
||||
* @param id Editor ID
|
||||
*/
|
||||
public setFocused(id: EditorId): void {
|
||||
this.setState({ focused: id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { AppState } from '../state';
|
||||
import { ChromeMac } from './chrome-mac';
|
||||
import { Commands } from './commands';
|
||||
import { Output } from './output';
|
||||
import { WelcomeTour } from './tour-welcome';
|
||||
|
||||
export interface HeaderProps {
|
||||
@@ -23,7 +22,6 @@ export class Header extends React.Component<HeaderProps, {}> {
|
||||
<ChromeMac appState={this.props.appState} />
|
||||
<header id='header'>
|
||||
<Commands key='commands' appState={this.props.appState} />
|
||||
<Output key='output' appState={this.props.appState} />
|
||||
</header>
|
||||
<WelcomeTour appState={this.props.appState} />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import { Mosaic, MosaicNode } from 'react-mosaic-component';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { Editors } from './editors';
|
||||
import { Output } from './output';
|
||||
|
||||
export interface WrapperProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface WrapperState {
|
||||
mosaicArrangement: MosaicNode<WrapperMosaicId>;
|
||||
}
|
||||
|
||||
export type WrapperMosaicId = 'output' | 'editors';
|
||||
|
||||
export class OutputEditorsWrapper extends React.Component<WrapperProps, WrapperState> {
|
||||
|
||||
private MOSAIC_ELEMENTS = {
|
||||
output: <Output appState={this.props.appState} />,
|
||||
editors: <Editors appState={this.props.appState} />
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
mosaicArrangement: {
|
||||
direction: 'column',
|
||||
first: 'output',
|
||||
second: 'editors',
|
||||
splitPercentage: 25,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Mosaic<WrapperMosaicId>
|
||||
renderTile={(id: string) => this.MOSAIC_ELEMENTS[id]}
|
||||
resize={{minimumPaneSizePercentage: 0}}
|
||||
value={this.state.mosaicArrangement}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onChange = (currentNode: any) => {
|
||||
const isConsoleShowing = currentNode.splitPercentage !== 0;
|
||||
|
||||
if (isConsoleShowing !== this.props.appState.isConsoleShowing) {
|
||||
this.props.appState.isConsoleShowing = isConsoleShowing;
|
||||
}
|
||||
|
||||
this.setState({mosaicArrangement: currentNode});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import { autorun } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import { MosaicContext } from 'react-mosaic-component';
|
||||
|
||||
import { OutputEntry } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
import { WrapperMosaicId } from './output-editors-wrapper';
|
||||
|
||||
|
||||
export interface CommandsProps {
|
||||
appState: AppState;
|
||||
// Used to keep testing conform
|
||||
renderTimestamp?: (ts: number) => string;
|
||||
}
|
||||
|
||||
@@ -18,6 +23,8 @@ export interface CommandsProps {
|
||||
*/
|
||||
@observer
|
||||
export class Output extends React.Component<CommandsProps, {}> {
|
||||
public static contextType = MosaicContext;
|
||||
public context: MosaicContext<WrapperMosaicId>;
|
||||
private outputRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: CommandsProps) {
|
||||
@@ -27,6 +34,23 @@ export class Output extends React.Component<CommandsProps, {}> {
|
||||
this.renderEntry = this.renderEntry.bind(this);
|
||||
}
|
||||
|
||||
|
||||
public componentDidMount() {
|
||||
autorun(() => {
|
||||
const { isConsoleShowing } = this.props.appState;
|
||||
|
||||
// this context should always exist, but mocking context in enzyme
|
||||
// is not fully supported, so this condition makes the tests pass
|
||||
if (this.context.mosaicActions && this.context.mosaicActions.expand) {
|
||||
if (!isConsoleShowing) {
|
||||
this.context.mosaicActions.expand(['first'], 0);
|
||||
} else {
|
||||
this.context.mosaicActions.expand(['first'], 25);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (this.outputRef.current) {
|
||||
this.outputRef.current.scrollTop = this.outputRef.current.scrollHeight;
|
||||
@@ -70,9 +94,12 @@ export class Output extends React.Component<CommandsProps, {}> {
|
||||
));
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isConsoleShowing, output } = this.props.appState;
|
||||
const className = isConsoleShowing ? 'output showing' : 'output';
|
||||
public render(): JSX.Element | null {
|
||||
const { output, isConsoleShowing } = this.props.appState;
|
||||
|
||||
if (!isConsoleShowing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The last 1000 lines
|
||||
const lines = output
|
||||
@@ -80,7 +107,7 @@ export class Output extends React.Component<CommandsProps, {}> {
|
||||
.map(this.renderEntry);
|
||||
|
||||
return (
|
||||
<div className={className} ref={this.outputRef}>
|
||||
<div className='output' ref={this.outputRef}>
|
||||
{lines}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
import { Callout, Card } from '@blueprintjs/core';
|
||||
import { shell } from 'electron';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface CreditsSettingsProps {
|
||||
appState: AppState;
|
||||
contributors?: Array<any>;
|
||||
}
|
||||
|
||||
export interface CreditsSettingsState {
|
||||
contributors: Array<Contributor>;
|
||||
}
|
||||
|
||||
export interface Contributor {
|
||||
url: string;
|
||||
api: string;
|
||||
login: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
bio: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage Credits-related preferences.
|
||||
*
|
||||
* @class CreditsSettings
|
||||
* @extends {React.Component<CreditsSettingsProps, {}>}
|
||||
* @extends {React.Component<CreditsSettingsProps, CreditsSettingsState>}
|
||||
*/
|
||||
export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
|
||||
private contributors: Array<any> | null = null;
|
||||
export class CreditsSettings extends React.Component<CreditsSettingsProps, CreditsSettingsState> {
|
||||
constructor(props: CreditsSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contributors: []
|
||||
};
|
||||
|
||||
this.getContributors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a list of contributors of Electron Fiddle.
|
||||
@@ -24,15 +47,9 @@ export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
public renderContributors(): Array<JSX.Element> {
|
||||
this.contributors = this.contributors
|
||||
|| this.props.contributors
|
||||
|| require('../../../static/contributors.json');
|
||||
const { contributors } = this.state;
|
||||
|
||||
if (!this.contributors || !Array.isArray(this.contributors)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.contributors.map(({ name, avatar, url, login, location, bio }) => {
|
||||
return contributors.map(({ name, avatar, url, login, location, bio }) => {
|
||||
const maybeLocation = location
|
||||
? <p className='location'>📍 {location}</p>
|
||||
: null;
|
||||
@@ -73,4 +90,14 @@ export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public async getContributors() {
|
||||
try {
|
||||
const contributorsFile = path.join(__dirname, '../../static/contributors.json');
|
||||
const contributors = await fs.readJSON(contributorsFile);
|
||||
this.setState({ contributors });
|
||||
} catch (error) {
|
||||
console.warn(`CreditsSettings: Fetching contributors failed`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
HTMLTable,
|
||||
IButtonProps,
|
||||
Icon,
|
||||
IconName
|
||||
IconName,
|
||||
Tooltip
|
||||
} from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
|
||||
import { RunnableVersion, VersionSource, VersionState } from '../../interfaces';
|
||||
import { normalizeVersion } from '../../utils/normalize-version';
|
||||
import { sortedElectronMap } from '../../utils/sorted-electron-map';
|
||||
import { AppState } from '../state';
|
||||
@@ -41,6 +42,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
this.handleDownloadAll = this.handleDownloadAll.bind(this);
|
||||
this.handleDeleteAll = this.handleDeleteAll.bind(this);
|
||||
this.handleChannelChange = this.handleChannelChange.bind(this);
|
||||
this.handleStateChange = this.handleStateChange.bind(this);
|
||||
this.handleDownloadClick = this.handleDownloadClick.bind(this);
|
||||
this.handleAddVersion = this.handleAddVersion.bind(this);
|
||||
|
||||
@@ -54,6 +56,24 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
this.props.appState.updateElectronVersions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in which channels should be displayed.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleStateChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { id, checked } = event.currentTarget;
|
||||
const { appState } = this.props;
|
||||
|
||||
if (!checked) {
|
||||
appState.statesToShow = appState.statesToShow.filter((s) => s !== id);
|
||||
} else {
|
||||
appState.statesToShow.push(id as VersionState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in which channels should be displayed.
|
||||
*
|
||||
@@ -66,9 +86,9 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
const { appState } = this.props;
|
||||
|
||||
if (!checked) {
|
||||
appState.versionsToShow = appState.versionsToShow.filter((c) => c !== id);
|
||||
appState.channelsToShow = appState.channelsToShow.filter((c) => c !== id);
|
||||
} else {
|
||||
appState.versionsToShow.push(id as ElectronReleaseChannel);
|
||||
appState.channelsToShow.push(id as ElectronReleaseChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +140,10 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
return (
|
||||
<div className='settings-electron'>
|
||||
<h2>Electron Settings</h2>
|
||||
{this.renderVersionOptions()}
|
||||
<Callout>
|
||||
{this.renderVersionChannelOptions()}
|
||||
{this.renderVersionStateOptions()}
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
{this.renderAdvancedButtons()}
|
||||
@@ -173,51 +196,103 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
|
||||
/**
|
||||
* Renders the various options for which versions should be displayed
|
||||
* in the small dropdown.
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderVersionOptions(): JSX.Element {
|
||||
private renderVersionStateOptions(): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const getIsChecked = (channel: ElectronReleaseChannel) => {
|
||||
return appState.versionsToShow.includes(channel);
|
||||
const getIsChecked = (state: VersionState) => {
|
||||
return appState.statesToShow.includes(state);
|
||||
};
|
||||
|
||||
return (
|
||||
<Callout>
|
||||
<FormGroup
|
||||
label='Include Electron versions from these release channels:'
|
||||
<FormGroup
|
||||
label='Include Electron versions that are:'
|
||||
>
|
||||
<Tooltip
|
||||
content='Always enabled'
|
||||
position='bottom'
|
||||
intent='primary'
|
||||
>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.stable)}
|
||||
label='Stable'
|
||||
id='Stable'
|
||||
onChange={this.handleChannelChange}
|
||||
checked={getIsChecked(VersionState.ready)}
|
||||
label='Ready'
|
||||
id='ready'
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
disabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.beta)}
|
||||
label='Beta'
|
||||
id='Beta'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.nightly)}
|
||||
label='Nightly'
|
||||
id='Nightly'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.unsupported)}
|
||||
label='Unsupported'
|
||||
id='Unsupported'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
</Tooltip>
|
||||
<Checkbox
|
||||
checked={getIsChecked(VersionState.downloading)}
|
||||
label='Downloading'
|
||||
id='downloading'
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(VersionState.unknown)}
|
||||
label='Not Downloaded'
|
||||
id='unknown'
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the various options for which versions should be displayed
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderVersionChannelOptions(): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
|
||||
const getIsChecked = (channel: ElectronReleaseChannel) => {
|
||||
return appState.channelsToShow.includes(channel);
|
||||
};
|
||||
|
||||
const getIsCurrentVersionReleaseChannel = (channel: ElectronReleaseChannel) => {
|
||||
return getReleaseChannel(appState.version) === channel;
|
||||
};
|
||||
|
||||
const channels = {
|
||||
stable: ElectronReleaseChannel.stable,
|
||||
beta: ElectronReleaseChannel.beta,
|
||||
nightly: ElectronReleaseChannel.nightly,
|
||||
unsupported: ElectronReleaseChannel.unsupported
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label='Include Electron versions from these release channels:'
|
||||
>
|
||||
{
|
||||
// tslint:disable-next-line:jsx-no-multiline-js
|
||||
Object.entries(channels).map(([_, channel]) => (
|
||||
<Tooltip
|
||||
content={`Can't disable channel of selected version (${appState.version})`}
|
||||
disabled={!getIsCurrentVersionReleaseChannel(channel)}
|
||||
position='bottom'
|
||||
intent='primary'
|
||||
key={channel}
|
||||
>
|
||||
<Checkbox
|
||||
checked={getIsChecked(channel)}
|
||||
label={channel}
|
||||
id={channel}
|
||||
onChange={this.handleChannelChange}
|
||||
disabled={getIsCurrentVersionReleaseChannel(channel)}
|
||||
inline={true}
|
||||
/>
|
||||
</Tooltip>
|
||||
))
|
||||
}
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -254,11 +329,16 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
private renderTableRows(): Array<JSX.Element | null> {
|
||||
const { versions, versionsToShow } = this.props.appState;
|
||||
const { versions, channelsToShow, statesToShow } = this.props.appState;
|
||||
|
||||
return sortedElectronMap<JSX.Element | null>(versions, (key, item) => {
|
||||
// Check if we want to show the version
|
||||
if (!versionsToShow.includes(getReleaseChannel(item))) {
|
||||
if (!channelsToShow.includes(getReleaseChannel(item))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we want to show the state
|
||||
if (!statesToShow.includes(item.state)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -273,20 +353,20 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable state indicator for an Electorn version
|
||||
* Returns a human-readable state indicator for an Electron version
|
||||
*
|
||||
* @param {ElectronVersion} item
|
||||
* @param {RunnableVersion} item
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderHumanState(item: ElectronVersion): JSX.Element {
|
||||
private renderHumanState(item: RunnableVersion): JSX.Element {
|
||||
const { state } = item;
|
||||
let icon: IconName = 'box';
|
||||
let humanState = 'Downloaded';
|
||||
|
||||
if (state === ElectronVersionState.downloading) {
|
||||
if (state === VersionState.downloading) {
|
||||
icon = 'cloud-download';
|
||||
humanState = 'Downloading';
|
||||
} else if (state === ElectronVersionState.unknown) {
|
||||
} else if (state === VersionState.unknown) {
|
||||
icon = 'cloud';
|
||||
humanState = 'Not downloaded';
|
||||
}
|
||||
@@ -303,10 +383,10 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
*
|
||||
* @private
|
||||
* @param {string} key
|
||||
* @param {ElectronVersion} item
|
||||
* @param {RunnableVersion} item
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderAction(key: string, item: ElectronVersion): JSX.Element {
|
||||
private renderAction(key: string, item: RunnableVersion): JSX.Element {
|
||||
const { state, source } = item;
|
||||
const { appState } = this.props;
|
||||
const buttonProps: IButtonProps = {
|
||||
@@ -318,7 +398,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
if (state === 'ready') {
|
||||
buttonProps.onClick = () => appState.removeVersion(key);
|
||||
buttonProps.icon = 'trash';
|
||||
buttonProps.text = source === ElectronVersionSource.local
|
||||
buttonProps.text = source === VersionSource.local
|
||||
? 'Remove'
|
||||
: 'Delete';
|
||||
} else if (state === 'downloading') {
|
||||
@@ -331,9 +411,9 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
buttonProps.loading = false;
|
||||
buttonProps.text = 'Download';
|
||||
buttonProps.icon = 'cloud-download';
|
||||
buttonProps.onClick = () => appState.setVersion(key);
|
||||
buttonProps.onClick = () => appState.downloadVersion(key);
|
||||
}
|
||||
|
||||
return <Button {...buttonProps} />;
|
||||
return <Button {...buttonProps} type={undefined} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Callout, Checkbox, FormGroup, InputGroup } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ExecutionSettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage execution-related preferences.
|
||||
*
|
||||
* @class ExecutionSettings
|
||||
* @extends {React.Component<ExecutionSettingsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {}> {
|
||||
constructor(props: ExecutionSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.handleDeleteDataChange = this.handleDeleteDataChange.bind(this);
|
||||
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(this);
|
||||
this.handleExecutionFlagChange = this.handleExecutionFlagChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change on whether or not the user data dir should be deleted
|
||||
* after a run.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleDeleteDataChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { checked } = event.currentTarget;
|
||||
this.props.appState.isKeepingUserDataDirs = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change on whether or not electron should log more things
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleElectronLoggingChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { checked } = event.currentTarget;
|
||||
this.props.appState.isEnablingElectronLogging = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in the execution flags run with the Electron executable
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleExecutionFlagChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { value } = event.currentTarget;
|
||||
const flags = value.split('|');
|
||||
this.props.appState.executionFlags = flags;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
isKeepingUserDataDirs,
|
||||
isEnablingElectronLogging,
|
||||
executionFlags = []
|
||||
} = this.props.appState;
|
||||
|
||||
const deleteUserDirLabel = `
|
||||
Whenever Electron runs, it creates a user data directory for cookies, the cache,
|
||||
and various other things that it needs to keep around. Since fiddles are usually
|
||||
just run once, we delete this directory after your fiddle exits. Enable this
|
||||
setting to keep the user data directories around.
|
||||
`.trim();
|
||||
const electronLoggingLabel = `
|
||||
There are some flags that Electron uses to log extra information both internally
|
||||
and through Chromium. Enable this option to make Fiddle produce those logs.`.trim();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Execution</h2>
|
||||
<Callout>
|
||||
These advanced settings control how Electron Fiddle executes your fiddles.
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
<FormGroup label={deleteUserDirLabel}>
|
||||
<Checkbox
|
||||
checked={isKeepingUserDataDirs}
|
||||
label='Do not delete user data directories.'
|
||||
onChange={this.handleDeleteDataChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
<FormGroup label={electronLoggingLabel}>
|
||||
<Checkbox
|
||||
checked={isEnablingElectronLogging}
|
||||
label='Enable advanced Electron logging.'
|
||||
onChange={this.handleElectronLoggingChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
<FormGroup>
|
||||
<p>
|
||||
Electron allows starting the executable with <a
|
||||
href='https://www.electronjs.org/docs/api/command-line-switches'
|
||||
>
|
||||
user-provided flags
|
||||
</a>
|
||||
, such as '--js-flags=--expose-gc'. Those can be added here as bar-separated (|)
|
||||
flags to run when you start your Fiddles.
|
||||
</p>
|
||||
<br />
|
||||
<InputGroup
|
||||
placeholder='--js-flags=--expose-gc|--lang=es'
|
||||
value={executionFlags.join('|')}
|
||||
onChange={this.handleExecutionFlagChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,10 @@ const ThemeSelect = Select.ofType<LoadedFiddleTheme>();
|
||||
* version.
|
||||
*
|
||||
* @param {string} query
|
||||
* @param {ElectronVersion} { version }
|
||||
* @param {RunnableVersion} { version }
|
||||
* @returns
|
||||
*/
|
||||
const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
|
||||
export const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
|
||||
return name.toLowerCase().includes(query.toLowerCase());
|
||||
};
|
||||
|
||||
@@ -31,11 +31,11 @@ const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
|
||||
* Helper method: Returns the <Select /> <MenuItem /> for Electron
|
||||
* versions.
|
||||
*
|
||||
* @param {ElectronVersion} item
|
||||
* @param {RunnableVersion} item
|
||||
* @param {IItemRendererProps} { handleClick, modifiers, query }
|
||||
* @returns
|
||||
*/
|
||||
const renderItem: ItemRenderer<LoadedFiddleTheme> = (item, { handleClick, modifiers, query }) => {
|
||||
export const renderItem: ItemRenderer<LoadedFiddleTheme> = (item, { handleClick, modifiers, query }) => {
|
||||
if (!modifiers.matchesPredicate) {
|
||||
return null;
|
||||
}
|
||||
@@ -76,6 +76,7 @@ export class AppearanceSettings extends React.Component<
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.openThemeFolder = this.openThemeFolder.bind(this);
|
||||
this.handleAddTheme = this.handleAddTheme.bind(this);
|
||||
|
||||
this.state = {
|
||||
themes: []
|
||||
@@ -111,8 +112,9 @@ export class AppearanceSettings extends React.Component<
|
||||
* @memberof AppearanceSettings
|
||||
*/
|
||||
public async createNewThemeFromCurrent(): Promise<boolean> {
|
||||
const { appState } = this.props;
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
const theme = await getTheme(this.props.appState.theme);
|
||||
const theme = await getTheme(appState.theme);
|
||||
|
||||
try {
|
||||
const namor = await fancyImport<any>('namor');
|
||||
@@ -128,6 +130,8 @@ export class AppearanceSettings extends React.Component<
|
||||
|
||||
shell.showItemInFolder(themePath);
|
||||
|
||||
this.setState({themes: await getAvailableThemes()});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Themes: Failed to create new theme from current`, error);
|
||||
@@ -190,13 +194,29 @@ export class AppearanceSettings extends React.Component<
|
||||
</a>. The easiest way to get started is to clone one of the two existing
|
||||
themes and to add your own colors.
|
||||
</p>
|
||||
<p>
|
||||
Additionally, if you wish to import a Monaco Editor theme, pick your JSON file and Fiddle will attempt to import it.
|
||||
</p>
|
||||
<Button
|
||||
onClick={this.createNewThemeFromCurrent}
|
||||
text='Create theme from current selection'
|
||||
icon='duplicate'
|
||||
/>
|
||||
<Button
|
||||
icon='document-open'
|
||||
onClick={this.handleAddTheme}
|
||||
text='Add a Monaco Editor theme'
|
||||
/>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the "add monaco theme" dialog
|
||||
*/
|
||||
public handleAddTheme(): void {
|
||||
this.props.appState.toggleAddMonacoThemeDialog();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Callout, Checkbox, FormGroup } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ConsoleSettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage console-related preferences.
|
||||
*
|
||||
* @class ConsoleSettings
|
||||
* @extends {React.Component<ConsoleSettingsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class ConsoleSettings extends React.Component<ConsoleSettingsProps, {}> {
|
||||
constructor(props: ConsoleSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.handleClearOnRunChange = this.handleClearOnRunChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change on whether or not the console should be cleared
|
||||
* before fiddle is executed.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleClearOnRunChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { checked } = event.currentTarget;
|
||||
this.props.appState.isClearingConsoleOnRun = checked;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isClearingConsoleOnRun } = this.props.appState;
|
||||
|
||||
const clearOnRunLabel = `
|
||||
Enable this option to automatically clear the console whenever you run your
|
||||
fiddle.`.trim();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>Console</h4>
|
||||
<Callout>
|
||||
<FormGroup label={clearOnRunLabel}>
|
||||
<Checkbox
|
||||
checked={isClearingConsoleOnRun}
|
||||
label='Clear on run.'
|
||||
onChange={this.handleClearOnRunChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,23 +16,25 @@ export interface GitHubSettingsProps {
|
||||
*/
|
||||
@observer
|
||||
export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
|
||||
constructor(props: GitHubSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.signIn = this.signIn.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "logged out" settings experience.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
public renderNotSignedIn(): JSX.Element {
|
||||
const signIn = () => {
|
||||
this.props.appState.isTokenDialogShowing = true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Callout>
|
||||
<p>
|
||||
Your fiddles can be published as public GitHub Gists -
|
||||
Your fiddles can be published as GitHub Gists -
|
||||
that way you can share your fiddles with the world!
|
||||
</p>
|
||||
<Button onClick={signIn} icon='log-in' text='Sign in'/>
|
||||
<Button onClick={this.signIn} icon='log-in' text='Sign in'/>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
@@ -72,4 +74,11 @@ export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply shows the GitHub Token dialog.``
|
||||
*/
|
||||
private signIn() {
|
||||
this.props.appState.isTokenDialogShowing = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { AppearanceSettings } from './settings-general-appearance';
|
||||
import { ConsoleSettings } from './settings-general-console';
|
||||
import { GitHubSettings } from './settings-general-github';
|
||||
|
||||
export interface GeneralSettingsProps {
|
||||
@@ -24,6 +25,8 @@ export class GeneralSettings extends React.Component<GeneralSettingsProps, {}> {
|
||||
<h2>General Settings</h2>
|
||||
<AppearanceSettings appState={this.props.appState} />
|
||||
<Divider />
|
||||
<ConsoleSettings appState={this.props.appState} />
|
||||
<Divider />
|
||||
<GitHubSettings appState={this.props.appState} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { Icon, MenuItem } from '@blueprintjs/core';
|
||||
import { Icon, IconName, MenuItem } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { CreditsSettings } from './settings-credits';
|
||||
import { ElectronSettings } from './settings-electron';
|
||||
import { ExecutionSettings } from './settings-execution';
|
||||
import { GeneralSettings } from './settings-general';
|
||||
|
||||
enum SettingsSections {
|
||||
General = 'General',
|
||||
Electron = 'Electron',
|
||||
Execution = 'Execution',
|
||||
Credits = 'Credits'
|
||||
}
|
||||
|
||||
const settingsSections = [
|
||||
SettingsSections.General,
|
||||
SettingsSections.Electron,
|
||||
SettingsSections.Execution,
|
||||
SettingsSections.Credits
|
||||
];
|
||||
|
||||
@@ -41,6 +44,16 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
|
||||
this.state = {
|
||||
section: SettingsSections.General
|
||||
};
|
||||
|
||||
this.closeSettingsPanel = this.closeSettingsPanel.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener('keyup', this.closeSettingsPanel, true);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('keyup', this.closeSettingsPanel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +74,10 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
|
||||
return <ElectronSettings appState={appState} />;
|
||||
}
|
||||
|
||||
if (section === SettingsSections.Execution) {
|
||||
return <ExecutionSettings appState={appState} />;
|
||||
}
|
||||
|
||||
if (section === SettingsSections.Credits) {
|
||||
return <CreditsSettings appState={appState} />;
|
||||
}
|
||||
@@ -120,13 +137,25 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
|
||||
* @param {SettingsSections} section
|
||||
* @memberof Settings
|
||||
*/
|
||||
private getIconForSection(section: SettingsSections) {
|
||||
private getIconForSection(section: SettingsSections): IconName {
|
||||
if (section === SettingsSections.Credits) {
|
||||
return 'heart';
|
||||
} else if (section === SettingsSections.Electron) {
|
||||
return 'floppy-disk';
|
||||
} else if (section === SettingsSections.Execution) {
|
||||
return 'play';
|
||||
}
|
||||
|
||||
return 'cog';
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger closing of the settings panel upon Esc
|
||||
*/
|
||||
private closeSettingsPanel(event: KeyboardEvent) {
|
||||
const { appState } = this.props;
|
||||
if (event.code === 'Escape') {
|
||||
appState.isSettingsShowing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { DOCS_DEMO_COMPONENTS } from './show-me/index';
|
||||
|
||||
export interface ShowMeProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**3
|
||||
* The root component for "show me" content - the fourth helpful
|
||||
* panel.
|
||||
*
|
||||
* @class ShowMe
|
||||
* @extends {React.Component<ShowMeProps, ShowMeState>}
|
||||
*/
|
||||
@observer
|
||||
export class ShowMe extends React.Component<ShowMeProps, {}> {
|
||||
public render() {
|
||||
const { currentDocsDemoPage: showMeName } = this.props.appState;
|
||||
const Content = DOCS_DEMO_COMPONENTS[showMeName] || DOCS_DEMO_COMPONENTS.DEFAULT;
|
||||
|
||||
return (
|
||||
<div className='show-me-panel'>
|
||||
<Content appState={this.props.appState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, Callout, Icon } from '@blueprintjs/core';
|
||||
import { remote } from 'electron';
|
||||
|
||||
import { renderMoreDocumentation } from './more-information';
|
||||
import { getSubsetOnly } from './subset-only';
|
||||
|
||||
/**
|
||||
* Show and hide button examples, only rendered on macOS
|
||||
*
|
||||
* @returns {JSX.Element | null}
|
||||
*/
|
||||
function ShowHide() {
|
||||
if (process.platform !== 'darwin') return null;
|
||||
|
||||
const onClick = () => {
|
||||
remote.app.hide();
|
||||
setTimeout(() => {
|
||||
remote.app.show();
|
||||
remote.app.focus();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>This button hides Electron Fiddle right away, showing it again in two seconds.</p>
|
||||
<Button
|
||||
id='show-hide'
|
||||
icon='eye-off'
|
||||
text='Hide Electron Fiddle'
|
||||
onClick={onClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export function ShowMeApp(_props: any): JSX.Element {
|
||||
const [ secondsLeft, setSeconds ] = React.useState(-1);
|
||||
|
||||
const playFocus = () => {
|
||||
setSeconds(3);
|
||||
setTimeout(() => setSeconds(2), 1000);
|
||||
setTimeout(() => setSeconds(1), 2000);
|
||||
setTimeout(() => {
|
||||
remote.app.focus();
|
||||
setSeconds(-1);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const [ paths, setPaths ] = React.useState('');
|
||||
const playPaths = () => {
|
||||
const pathsToQuery = [
|
||||
'home',
|
||||
'appData',
|
||||
'userData',
|
||||
'temp',
|
||||
'downloads',
|
||||
'desktop'
|
||||
];
|
||||
|
||||
let result = '';
|
||||
pathsToQuery.forEach((item) => {
|
||||
result += `${item}: ${remote.app.getPath(item as any)}\n`;
|
||||
});
|
||||
|
||||
setPaths(result);
|
||||
};
|
||||
|
||||
const [ metrics, setMetrics ] = React.useState('');
|
||||
const playMetrics = () => {
|
||||
setMetrics(JSON.stringify(remote.app.getAppMetrics(), undefined, 2));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon icon='help' iconSize={40} style={{ float: 'left', margin: '0 10px 0 0' }} />
|
||||
<p className='bp3r-running-text'>
|
||||
The <code>app</code> module controls the app's application life-cycle. Most of the
|
||||
events and methods available on this module are responsible for handling how your
|
||||
interacts with the operating system or to set application-wide settings.
|
||||
</p>
|
||||
<h3>API Demos</h3>
|
||||
{getSubsetOnly('app')}
|
||||
<Callout
|
||||
title='Hiding, Showing, Focussing'
|
||||
icon='eye-open'
|
||||
>
|
||||
<p>
|
||||
The app can ask the operating system for window focus. On macOS, it can additionally
|
||||
request that the app be hidden or shown. Give it a try: Click on the button below,
|
||||
focus another app, and wait for two seconds to see Electron Fiddle become the focused
|
||||
app again.
|
||||
</p>
|
||||
<Button
|
||||
id='focus'
|
||||
icon='lightbulb'
|
||||
text={`Focus Electron Fiddle${secondsLeft > 0 ? ` in ${secondsLeft}s` : ''}`}
|
||||
onClick={playFocus}
|
||||
/>
|
||||
<ShowHide />
|
||||
</Callout>
|
||||
<Callout
|
||||
title='Paths'
|
||||
icon='folder-open'
|
||||
>
|
||||
<p>
|
||||
Need to query information about various paths in a cross-platform manner? Electron
|
||||
can help. The button queries the operating system for some of them.
|
||||
</p>
|
||||
<Button
|
||||
id='special-paths'
|
||||
icon='play'
|
||||
text={`Get special directory paths`}
|
||||
onClick={playPaths}
|
||||
/>
|
||||
<pre id='special-paths-content'>
|
||||
{paths}
|
||||
</pre>
|
||||
</Callout>
|
||||
<Callout
|
||||
title='Process & Device Information'
|
||||
icon='pulse'
|
||||
>
|
||||
<p>
|
||||
Need to query information about the process or system? The <code>app</code> module lets
|
||||
developers query for information about the running app, hardware, and operating system.
|
||||
A good example are process metrics.
|
||||
</p>
|
||||
<Button
|
||||
id='process-metrics'
|
||||
icon='play'
|
||||
text={`Get process metrics`}
|
||||
onClick={playMetrics}
|
||||
/>
|
||||
<pre id='process-metrics-content'>
|
||||
{metrics}
|
||||
</pre>
|
||||
</Callout>
|
||||
{renderMoreDocumentation()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Button, Icon } from '@blueprintjs/core';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DocsDemoPage } from '../../../interfaces';
|
||||
import { AppState } from '../../state';
|
||||
import { DOCS_DEMO_COMPONENTS, DOCS_DEMO_NAMES } from './index';
|
||||
import { renderMoreDocumentation } from './more-information';
|
||||
|
||||
export function ShowMeDefault(props: { appState: AppState }): JSX.Element {
|
||||
const showMeMenu: Array<JSX.Element> = Object.keys(DOCS_DEMO_COMPONENTS)
|
||||
.filter((key) => key !== DocsDemoPage.DEFAULT)
|
||||
.map((key) => {
|
||||
return (
|
||||
<li key={key}>
|
||||
<Button
|
||||
minimal={true}
|
||||
icon='play'
|
||||
text={DOCS_DEMO_NAMES[key]}
|
||||
onClick={() => (props.appState.currentDocsDemoPage = key as DocsDemoPage)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon icon='help' iconSize={40} style={{ float: 'left', margin: '0 10px 0 0' }} />
|
||||
<p className='bp3r-running-text'>
|
||||
This panel offers useful information about Electron APIs – and
|
||||
easy way to try some of the methods it offers. Fiddle has an example fiddle
|
||||
for every module available in Electron.
|
||||
</p>
|
||||
<p>
|
||||
Clicking on any of the modules below will open up a fiddle showcasing
|
||||
that particular module.
|
||||
</p>
|
||||
<ul className='show-me-list'>
|
||||
{...showMeMenu}
|
||||
</ul>
|
||||
{renderMoreDocumentation()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DocsDemoPage } from '../../../interfaces';
|
||||
|
||||
import { ShowMeApp } from './app';
|
||||
import { ShowMeDefault } from './default';
|
||||
|
||||
export const DOCS_DEMO_COMPONENTS: Record<DocsDemoPage, any> = {
|
||||
DEFAULT: ShowMeDefault,
|
||||
DEMO_APP: ShowMeApp
|
||||
};
|
||||
|
||||
export const DOCS_DEMO_NAMES: Record<DocsDemoPage, string> = {
|
||||
DEFAULT: 'Home',
|
||||
DEMO_APP: 'App Demos'
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { shell } from 'electron';
|
||||
|
||||
/**
|
||||
* Renders a "more documentation" block, typically used at the end of a "show me" documentation piece.
|
||||
*
|
||||
* @export
|
||||
* @param {string} [url='electronjs.org/docs']
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function renderMoreDocumentation(
|
||||
url: string = 'electronjs.org/docs'
|
||||
): JSX.Element {
|
||||
const fullUrl = `https://${url}`;
|
||||
const gitHubUrl = `https://github.com/electron/fiddle`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<p className='b3-running-text'>
|
||||
For more documentation, visit <a id='open-url' onClick={() => shell.openExternal(fullUrl)}>{url}</a>, where
|
||||
you can find the full documentation for Electron.
|
||||
</p>
|
||||
<p className='bp3-text-muted'>
|
||||
By the way, Electron Fiddle and the documentation you see here is entirely open source. If you
|
||||
have ideas on how to improve it, we'd love to have your contributions! You can find the
|
||||
repository on <a id='open-github' onClick={() => shell.openExternal(gitHubUrl)}>GitHub</a>.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { shell } from 'electron';
|
||||
|
||||
import { getDocsUrlForModule } from '../../../utils/docs-urls';
|
||||
|
||||
/**
|
||||
* Returns a common string we use as a disclaimer to explain that this set of demos
|
||||
* is only meant to be a short demonstration, not displaying all of the modules
|
||||
* abilities.
|
||||
*
|
||||
* @export
|
||||
* @param {string} moduleName
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function getSubsetOnly(moduleName: string): JSX.Element {
|
||||
const { full, short } = getDocsUrlForModule(moduleName);
|
||||
|
||||
return (
|
||||
<p className='bp3-running-text'>
|
||||
The following demos display only a subset of what the <code>{moduleName}</code>
|
||||
module is capable of. If you want to see its full abilities, check out the
|
||||
documentation on <a id='open-url' onClick={() => shell.openExternal(full)}>{short}</a>.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { Dialog } from './dialog';
|
||||
import { Tour, TourScriptStep, TourStepGetButtonParams } from './tour';
|
||||
|
||||
export interface WelcomeTourProps {
|
||||
@@ -19,33 +19,33 @@ export interface WelcomeTourState {
|
||||
*
|
||||
* @returns {Set<TourScriptStep>}
|
||||
*/
|
||||
function getWelcomeTour(): Set<TourScriptStep> {
|
||||
export function getWelcomeTour(): Set<TourScriptStep> {
|
||||
return new Set([
|
||||
{
|
||||
name: 'fiddle-editors',
|
||||
selector: 'div.mosaic-root',
|
||||
title: '📝 Fiddle Editors',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📝 Fiddle Editors</h4>
|
||||
<>
|
||||
<p>
|
||||
Electron Fiddle allows you to build little experiments and mini-apps with
|
||||
Electron. Each Fiddle has three files: A main script, a renderer script,
|
||||
and an HTML file.
|
||||
Electron. Each Fiddle has at least three of these files: A main script, a
|
||||
renderer script, a preload script, and an HTML file.
|
||||
</p>
|
||||
<p>
|
||||
If you <code>require()</code> a module, Fiddle will install
|
||||
it automatically. It will also automatically provide you with autocomplete
|
||||
information for the <code>electron</code> module.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'select-versions',
|
||||
selector: 'select.select-versions',
|
||||
selector: 'button.version-chooser',
|
||||
title: '📇 Choose an Electron Version',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📇 Choose an Electron Version</h4>
|
||||
<>
|
||||
<p>
|
||||
Electron Fiddle knows about all released Electron versions, downloading
|
||||
your versions automatically in the background.
|
||||
@@ -54,27 +54,25 @@ function getWelcomeTour(): Set<TourScriptStep> {
|
||||
Open the preferences to see all available versions and delete those previously
|
||||
downloaded.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'button-run',
|
||||
selector: 'button.button-run',
|
||||
selector: '.button-run',
|
||||
title: '🚀 Run Your Fiddle',
|
||||
content: (
|
||||
<div>
|
||||
<h4>🚀 Run Your Fiddle</h4>
|
||||
<p>
|
||||
Hit this button to give your Fiddle a try and start it.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
Hit this button to give your Fiddle a try and start it.
|
||||
</p>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'button-publish',
|
||||
selector: 'button.button-publish',
|
||||
selector: '.button-publish',
|
||||
title: '🗺 Share Your Fiddle',
|
||||
content: (
|
||||
<div>
|
||||
<h4>🗺 Share Your Fiddle</h4>
|
||||
<>
|
||||
<p>
|
||||
Like what you've built? You can save your Fiddle as a public GitHub Gist,
|
||||
allowing other users to load it by pasting the URL into the address bar.
|
||||
@@ -85,41 +83,39 @@ function getWelcomeTour(): Set<TourScriptStep> {
|
||||
You can also package your Fiddle as a standalone binary or as an installer
|
||||
from the "Tasks" menu.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'first-time-electron',
|
||||
selector: 'div.mosaic-root',
|
||||
title: '👋 Getting Started With Electron?',
|
||||
content: (
|
||||
<div>
|
||||
<h4>👋 Getting Started With Electron?</h4>
|
||||
<p>
|
||||
We've finished our tour of Electron Fiddle, but if this is your
|
||||
first time using Electron, we could introduce you to its basics.
|
||||
Interested?
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
We've finished our tour of Electron Fiddle, but if this is your
|
||||
first time using Electron, we could introduce you to its basics.
|
||||
Interested?
|
||||
</p>
|
||||
),
|
||||
getButtons: ({ stop, advance }: TourStepGetButtonParams): Array<JSX.Element> => {
|
||||
return [
|
||||
<button key='btn-stop' onClick={stop}>I'm good!</button>,
|
||||
<button key='btn-adv' onClick={advance}>Electron Basics</button>
|
||||
<Button key='btn-stop' onClick={stop} text="I'm good!" icon='stop' />,
|
||||
<Button key='btn-adv' onClick={advance} text='Electron Basics' icon='help' />
|
||||
];
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'main-editor',
|
||||
selector: 'div.mosaic-window.main',
|
||||
title: '📝 Main Script',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📝 Main Script</h4>
|
||||
<>
|
||||
<p>
|
||||
Every Electron app starts with a main script, very similar to how
|
||||
a Node.js application is started. The main script runs in the "main
|
||||
process". To display a user interface, the main process creates renderer
|
||||
processes – usually in the form of windows, which Electron calls
|
||||
<code>BrowserWindow</code>.
|
||||
<code>BrowserWindow</code>.
|
||||
</p>
|
||||
<p>
|
||||
To get started, pretend that the main process is just like a Node.js
|
||||
@@ -131,42 +127,40 @@ function getWelcomeTour(): Set<TourScriptStep> {
|
||||
The default fiddle creates a new <code>BrowserWindow</code> and loads
|
||||
an HTML file.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'html-editor',
|
||||
selector: 'div.mosaic-window.html',
|
||||
title: '📝 HTML',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📝 HTML</h4>
|
||||
<p>
|
||||
In the default fiddle, this HTML file is loaded in the
|
||||
<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
|
||||
in a browser will work here, too. In addition, Electron allows you
|
||||
to execute Node.js code. Take a close look at the
|
||||
<code><script /></code> tag and notice how we can call <code>
|
||||
require()</code> like we would in Node.js.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
In the default fiddle, this HTML file is loaded in the
|
||||
<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
|
||||
in a browser will work here, too. In addition, Electron allows you
|
||||
to execute Node.js code. Take a close look at the
|
||||
<code><script /></code> tag and notice how we can call <code>
|
||||
require()</code> like we would in Node.js.
|
||||
</p>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'renderer-editor',
|
||||
selector: 'div.mosaic-window.renderer',
|
||||
title: '📝 Renderer Script',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📝 Renderer Script</h4>
|
||||
<>
|
||||
<p>
|
||||
This is the script we just required from the HTML file. In here, you can
|
||||
do anything that works in Node.js <i>and</i> anything that works in a browser.
|
||||
</p>
|
||||
<p>
|
||||
By the way: If you want to use an <code>npm</code> module here, just
|
||||
<code>require</code> it. Electron Fiddle will automatically detect that you
|
||||
<code>require</code> it. Electron Fiddle will automatically detect that you
|
||||
requested a module and install it as soon as you run your fiddle.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
]);
|
||||
@@ -206,6 +200,25 @@ export class WelcomeTour extends React.Component<WelcomeTourProps, WelcomeTourSt
|
||||
this.setState({ isTourStarted: true });
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
key='cancel'
|
||||
onClick={this.stopTour}
|
||||
icon='cross'
|
||||
text={`I'll figure it out`}
|
||||
/>
|
||||
<Button
|
||||
key='ok'
|
||||
onClick={this.startTour}
|
||||
icon='presentation'
|
||||
text='Show me around'
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isTourShowing } = this.props.appState;
|
||||
const { isTourStarted } = this.state;
|
||||
@@ -213,28 +226,29 @@ export class WelcomeTour extends React.Component<WelcomeTourProps, WelcomeTourSt
|
||||
if (!isTourShowing) return null;
|
||||
|
||||
if (!isTourStarted) {
|
||||
const buttons = [
|
||||
<button key='cancel' className='btn-close' onClick={this.stopTour}>I'll figure it out</button>,
|
||||
<button key='ok' className='btn-ok' onClick={this.startTour}>Show me around</button>
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
key='welcome-tour-dialog'
|
||||
isCentered={true}
|
||||
isShowing={true}
|
||||
isShowingBackdrop={true}
|
||||
buttons={buttons}
|
||||
isOpen={true}
|
||||
>
|
||||
<h4>🙋 Hey There!</h4>
|
||||
<p>
|
||||
Welcome to Electron Fiddle! If you're new to the app,
|
||||
we'd like to give you a brief tour of its features.
|
||||
</p>
|
||||
<p>
|
||||
We won't show this dialog again, but you can always
|
||||
find the tour in the Help menu.
|
||||
</p>
|
||||
<div className={Classes.DIALOG_HEADER}>
|
||||
<h4 className={Classes.HEADING}>🙋 Hey There!</h4>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<p>
|
||||
Welcome to Electron Fiddle! If you're new to the app,
|
||||
we'd like to give you a brief tour of its features.
|
||||
</p>
|
||||
<p>
|
||||
We won't show this dialog again, but you can always
|
||||
find the tour in the Help menu.
|
||||
</p>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
{this.buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import * as React from 'react';
|
||||
import { invertPosition, positionForRect } from '../../utils/position-for-rect';
|
||||
import { Dialog } from './dialog';
|
||||
|
||||
import { positionForRect } from '../../utils/position-for-rect';
|
||||
|
||||
export interface TourScriptStep {
|
||||
name: string;
|
||||
selector: string;
|
||||
title: string;
|
||||
content: JSX.Element;
|
||||
getButtons?: (handlers: TourStepGetButtonParams) => Array<JSX.Element>;
|
||||
}
|
||||
@@ -116,12 +118,29 @@ export class Tour extends React.Component<TourProps, TourState> {
|
||||
|
||||
// No? Fine! Are we at the end of the tour?
|
||||
return this.props.tour.size === this.state.i
|
||||
? [
|
||||
<button key='btn-stop' className='btn-stop' onClick={this.stop}>Finish Tour</button>
|
||||
]
|
||||
: [
|
||||
<button key='btn-stop' className='btn-stop' onClick={this.stop}>Stop Tour</button>,
|
||||
<button key='btn-adv' className='btn-adv' onClick={this.advance}>Continue</button>
|
||||
? [(
|
||||
<Button
|
||||
icon='tick-circle'
|
||||
onClick={this.stop}
|
||||
key='btn-stop'
|
||||
text='Finish Tour'
|
||||
/>
|
||||
)] : [
|
||||
(
|
||||
<Button
|
||||
icon='stop'
|
||||
key='btn-stop'
|
||||
onClick={this.stop}
|
||||
text='Stop Tour'
|
||||
/>
|
||||
), (
|
||||
<Button
|
||||
icon='step-forward'
|
||||
key='btn-adv'
|
||||
onClick={this.advance}
|
||||
text='Continue'
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -142,13 +161,31 @@ export class Tour extends React.Component<TourProps, TourState> {
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
width: size.width - margin,
|
||||
margin: `${margin}px`
|
||||
margin: `${margin}px`,
|
||||
zIndex: 1000,
|
||||
};
|
||||
const arrow = invertPosition(position.type);
|
||||
|
||||
return (
|
||||
<Dialog key={step.name} buttons={buttons} style={style} arrow={arrow}>
|
||||
{step.content}
|
||||
<Dialog
|
||||
key={step.name}
|
||||
isOpen={true}
|
||||
style={style}
|
||||
portalClassName='tour-portal'
|
||||
backdropProps={{ style: { visibility: 'hidden' }}}
|
||||
>
|
||||
<div className={Classes.DIALOG_HEADER}>
|
||||
<h4 className={Classes.HEADING}>
|
||||
{step.title}
|
||||
</h4>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
{step.content}
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -161,9 +198,7 @@ export class Tour extends React.Component<TourProps, TourState> {
|
||||
*/
|
||||
private getStep(step: TourScriptStep): JSX.Element | null {
|
||||
const { selector } = step;
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) return null;
|
||||
|
||||
const element = document.querySelector(selector) || document.body!;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const { top, left, width, height } = rect;
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Button, IButtonGroupProps, MenuItem } from '@blueprintjs/core';
|
||||
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { RunnableVersion, VersionSource, VersionState } from '../../interfaces';
|
||||
import { highlightText } from '../../utils/highlight-text';
|
||||
import { AppState } from '../state';
|
||||
|
||||
const ElectronVersionSelect = Select.ofType<RunnableVersion>();
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> label for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {RunnableVersion} { source, state }
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getItemLabel({ source, state, name }: RunnableVersion): string {
|
||||
let label = '';
|
||||
|
||||
if (source === VersionSource.local) {
|
||||
label = name || 'Local';
|
||||
} else {
|
||||
if (state === VersionState.unknown) {
|
||||
label = `Not downloaded`;
|
||||
} else if (state === VersionState.ready) {
|
||||
label = `Downloaded`;
|
||||
} else if (state === VersionState.downloading) {
|
||||
label = `Downloading`;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> icon for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {RunnableVersion} { state }
|
||||
* @returns
|
||||
*/
|
||||
export function getItemIcon({ state }: RunnableVersion) {
|
||||
return state === 'ready'
|
||||
? 'saved'
|
||||
: state === 'downloading' ? 'cloud-download' : 'cloud';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> predicate for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {string} query
|
||||
* @param {RunnableVersion} { version }
|
||||
* @returns
|
||||
*/
|
||||
export const filterItem: ItemPredicate<RunnableVersion> = (query, { version }) => {
|
||||
return version.toLowerCase().includes(query.toLowerCase());
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> <MenuItem /> for Electron
|
||||
* versions.
|
||||
*
|
||||
* @param {RunnableVersion} item
|
||||
* @param {IItemRendererProps} { handleClick, modifiers, query }
|
||||
* @returns
|
||||
*/
|
||||
export const renderItem: ItemRenderer<RunnableVersion> = (item, { handleClick, modifiers, query }) => {
|
||||
if (!modifiers.matchesPredicate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
text={highlightText(item.version, query)}
|
||||
key={item.version}
|
||||
onClick={handleClick}
|
||||
label={getItemLabel(item)}
|
||||
icon={getItemIcon(item)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface VersionSelectState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface VersionSelectProps {
|
||||
appState: AppState;
|
||||
disabled?: boolean;
|
||||
currentVersion: RunnableVersion;
|
||||
onVersionSelect: (version: RunnableVersion) => void;
|
||||
buttonGroupProps?: IButtonGroupProps;
|
||||
itemDisabled?: keyof RunnableVersion | ((item: RunnableVersion, index: number) => boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* A dropdown allowing the selection of Electron versions. The actual
|
||||
* download is managed in the state.
|
||||
*
|
||||
* @class VersionSelect
|
||||
* @extends {React.Component<VersionSelectProps, VersionSelectState>}
|
||||
*/
|
||||
@observer
|
||||
export class VersionSelect extends React.Component<VersionSelectProps, VersionSelectState> {
|
||||
public render() {
|
||||
const { currentVersion, itemDisabled } = this.props;
|
||||
const { version } = currentVersion;
|
||||
|
||||
return (
|
||||
<ElectronVersionSelect
|
||||
filterable={true}
|
||||
items={this.props.appState.versionsToShow}
|
||||
itemRenderer={renderItem}
|
||||
itemPredicate={filterItem}
|
||||
itemDisabled={itemDisabled}
|
||||
onItemSelect={this.props.onVersionSelect}
|
||||
noResults={<MenuItem disabled={true} text='No results.' />}
|
||||
disabled={!!this.props.disabled}
|
||||
>
|
||||
<Button
|
||||
className='version-chooser'
|
||||
text={`Electron v${version}`}
|
||||
icon={getItemIcon(currentVersion)}
|
||||
disabled={!!this.props.disabled}
|
||||
/>
|
||||
</ElectronVersionSelect>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { remote } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { MosaicNode } from 'react-mosaic-component';
|
||||
|
||||
import { EditorId, MosaicId } from '../interfaces';
|
||||
import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
|
||||
|
||||
// Reminder: When testing, this file is mocked in tests/setup.js
|
||||
|
||||
export const USER_DATA_PATH = remote.app.getPath('userData');
|
||||
export const CONFIG_PATH = path.join(remote.app.getPath('home'), '.electron-fiddle');
|
||||
|
||||
export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
|
||||
direction: 'row',
|
||||
first: EditorId.main,
|
||||
second: {
|
||||
direction: 'column',
|
||||
first: EditorId.renderer,
|
||||
second: EditorId.html,
|
||||
}
|
||||
};
|
||||
|
||||
export const DEFAULT_CLOSED_PANELS: Partial<Record<MosaicId, EditorBackup | true>> = {
|
||||
docsDemo: true,
|
||||
preload: getEditorBackup(EditorId.preload),
|
||||
css: getEditorBackup(EditorId.css),
|
||||
};
|
||||
|
||||
export const EMPTY_EDITOR_CONTENT = {
|
||||
html: '<!-- Empty -->',
|
||||
js: '// Empty',
|
||||
css: '/* Empty */'
|
||||
};
|
||||
|
||||
export const ELECTRON_ORG = 'electron';
|
||||
export const ELECTRON_REPO = 'electron';
|
||||
+14
-18
@@ -1,32 +1,28 @@
|
||||
export const enum ContentNames {
|
||||
HTML = 'html',
|
||||
RENDERER = 'renderer',
|
||||
MAIN = 'main'
|
||||
}
|
||||
import { EditorId } from '../interfaces';
|
||||
|
||||
/**
|
||||
* Returns expected content for a given name.
|
||||
*
|
||||
* @export
|
||||
* @param {ContentNames} name
|
||||
* @param {EditorId} name
|
||||
* @param {string} [version]
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getContent(
|
||||
name: ContentNames,
|
||||
version?: string
|
||||
name: EditorId,
|
||||
version?: string,
|
||||
): Promise<string> {
|
||||
if (name === ContentNames.HTML) {
|
||||
if (name === EditorId.html) {
|
||||
return (await import('../content/html')).html;
|
||||
}
|
||||
|
||||
if (name === ContentNames.RENDERER) {
|
||||
if (name === EditorId.renderer) {
|
||||
return (await import('../content/renderer')).renderer;
|
||||
}
|
||||
|
||||
if (name === ContentNames.MAIN) {
|
||||
if (name === EditorId.main) {
|
||||
// We currently only distinguish between loadFile
|
||||
// and loadURL. Todo: Properly version the quick-start.
|
||||
// and loadURL. TODO: Properly version the quick-start.
|
||||
if (version && version.startsWith('1.')) {
|
||||
return (await import('../content/main-1-x-x')).main;
|
||||
}
|
||||
@@ -40,18 +36,18 @@ export async function getContent(
|
||||
/**
|
||||
* Did the content change?
|
||||
*
|
||||
* @param {ContentNames} name
|
||||
* @param {EditorId} name
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function isContentUnchanged(name: ContentNames): Promise<boolean> {
|
||||
export async function isContentUnchanged(name: EditorId): Promise<boolean> {
|
||||
if (!window.ElectronFiddle || !window.ElectronFiddle.app) return false;
|
||||
|
||||
const values = await window.ElectronFiddle.app.getValues({ include: false });
|
||||
const values = await window.ElectronFiddle.app.getEditorValues({ include: false });
|
||||
|
||||
// Handle main case, which needs to check both possible versions
|
||||
if (name === ContentNames.MAIN) {
|
||||
const isChanged1x = await getContent(ContentNames.MAIN, '1.0') === values.main;
|
||||
const isChangedOther = await getContent(ContentNames.MAIN) === values.main;
|
||||
if (name === EditorId.main) {
|
||||
const isChanged1x = await getContent(EditorId.main, '1.0') === values.main;
|
||||
const isChangedOther = await getContent(EditorId.main) === values.main;
|
||||
|
||||
return isChanged1x || isChangedOther;
|
||||
} else {
|
||||
|
||||
+80
-22
@@ -2,30 +2,61 @@ import * as fsType from 'fs-extra';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as path from 'path';
|
||||
|
||||
import { USER_DATA_PATH } from '../constants';
|
||||
import { RunnableVersion, VersionSource } from '../interfaces';
|
||||
import { callIn } from '../utils/call-in';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { normalizeVersion } from '../utils/normalize-version';
|
||||
import { USER_DATA_PATH } from './constants';
|
||||
|
||||
const definitionPath = path.join(USER_DATA_PATH, 'electron-typedef');
|
||||
|
||||
|
||||
/**
|
||||
* Fetch TypeScript definitions for the current version of Electron (online)
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export function fetchTypeDefinitions(version: string): Promise<string> {
|
||||
export async function fetchTypeDefinitions(version: string): Promise<string> {
|
||||
const url = `https://unpkg.com/electron@${version}/electron.d.ts`;
|
||||
|
||||
return window.fetch(url)
|
||||
.then((response) => response.text())
|
||||
.catch((error) => {
|
||||
console.warn(`Fetch Types: Could not fetch definitions`, error);
|
||||
return '';
|
||||
});
|
||||
let text: string;
|
||||
try {
|
||||
const response = await window.fetch(url);
|
||||
text = await response.text();
|
||||
} catch (error) {
|
||||
console.warn(`Fetch Types: Could not fetch definitions`, error);
|
||||
return '';
|
||||
}
|
||||
|
||||
// for invalid packa
|
||||
if (text.includes('Cannot find package')) {
|
||||
console.warn(`Fetch Types: ${text}`);
|
||||
return '';
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the type definition for a given version
|
||||
*
|
||||
* @param version
|
||||
*/
|
||||
export async function removeTypeDefsForVersion(version: string) {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
const _version = normalizeVersion(version);
|
||||
const typeDefsDir = path.dirname(getOfflineTypeDefinitionPath(_version));
|
||||
|
||||
if (fs.existsSync(typeDefsDir)) {
|
||||
try {
|
||||
await fs.remove(typeDefsDir);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the path for offline TypeScript definitions
|
||||
*
|
||||
@@ -48,27 +79,25 @@ export async function getOfflineTypeDefinitions(version: string): Promise<boolea
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TypeScript defintions for a version of Electron. If none can't be
|
||||
* Get TypeScript definitions for a version of Electron. If none can be
|
||||
* found, returns null.
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function getTypeDefinitions(version: string): Promise<string | null> {
|
||||
export async function getDownloadedVersionTypeDefs(version: RunnableVersion): Promise<string | null> {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
await fs.mkdirp(definitionPath);
|
||||
await fs.mkdirp(definitionPath);
|
||||
const offlinePath = getOfflineTypeDefinitionPath(version.version);
|
||||
|
||||
const offlinePath = getOfflineTypeDefinitionPath(version);
|
||||
|
||||
if (await getOfflineTypeDefinitions(version)) {
|
||||
if (await getOfflineTypeDefinitions(version.version)) {
|
||||
try {
|
||||
return await fs.readFile(offlinePath, 'utf-8');
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
const typeDefs = await fetchTypeDefinitions(version);
|
||||
|
||||
const typeDefs = await fetchTypeDefinitions(version.version);
|
||||
if (typeDefs && typeDefs.length > 0) {
|
||||
try {
|
||||
await fs.outputFile(offlinePath, typeDefs);
|
||||
@@ -79,17 +108,27 @@ export async function getTypeDefinitions(version: string): Promise<string | null
|
||||
return typeDefs;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLocalVersionTypeDefs(version: RunnableVersion) {
|
||||
if (version.source === VersionSource.local && !!version.localPath) {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
const typesPath = getLocalTypePathForVersion(version);
|
||||
if (!!typesPath && fs.existsSync(typesPath)) {
|
||||
return fs.readFile(typesPath, 'utf-8');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to update the editor with type definitions.
|
||||
*
|
||||
* @param {string} version
|
||||
*/
|
||||
export async function updateEditorTypeDefinitions(version: string, i: number = 0): Promise<void> {
|
||||
export async function updateEditorTypeDefinitions(version: RunnableVersion, i: number = 0): Promise<void> {
|
||||
const defer = async (): Promise<void> => {
|
||||
if (i > 10) {
|
||||
console.warn(`Fetch Types: Failed, dependencies do not exist`);
|
||||
@@ -106,17 +145,36 @@ export async function updateEditorTypeDefinitions(version: string, i: number = 0
|
||||
const { app } = window.ElectronFiddle;
|
||||
const monaco: typeof MonacoType = app.monaco!;
|
||||
const typeDefDisposable: MonacoType.IDisposable = app.typeDefDisposable!;
|
||||
const typeDefs = await getTypeDefinitions(version);
|
||||
|
||||
const getTypeDefs = (version.source === VersionSource.local) ?
|
||||
getLocalVersionTypeDefs : getDownloadedVersionTypeDefs;
|
||||
|
||||
const typeDefs = await getTypeDefs(version);
|
||||
|
||||
if (typeDefDisposable) {
|
||||
typeDefDisposable.dispose();
|
||||
}
|
||||
|
||||
if (typeDefs) {
|
||||
console.log(`Fetch Types: Updating Monaco types with electron.d.ts@${version}`);
|
||||
console.log(`Fetch Types: Updating Monaco types with electron.d.ts@${version.version}`);
|
||||
const disposable = monaco.languages.typescript.javascriptDefaults.addExtraLib(typeDefs);
|
||||
window.ElectronFiddle.app.typeDefDisposable = disposable;
|
||||
} else {
|
||||
console.log(`Fetch Types: No type definitions for ${version} 😢`);
|
||||
console.log(`Fetch Types: No type definitions for ${version.version} 😢`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalTypePathForVersion(version: RunnableVersion) {
|
||||
if (version.localPath) {
|
||||
return path.join(
|
||||
version.localPath,
|
||||
'gen',
|
||||
'electron',
|
||||
'tsc',
|
||||
'typings',
|
||||
'electron.d.ts'
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import * as fsType from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../constants';
|
||||
import { EditorValues, Files, FileTransform } from '../interfaces';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
|
||||
import { DEFAULT_OPTIONS, PackageJsonOptions } from '../utils/get-package';
|
||||
import { getTitle } from '../utils/get-title';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { ipcRendererManager } from './ipc';
|
||||
import { AppState } from './state';
|
||||
@@ -18,6 +17,11 @@ export class FileManager {
|
||||
this.openFiddle = this.openFiddle.bind(this);
|
||||
this.saveFiddle = this.saveFiddle.bind(this);
|
||||
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.FS_OPEN_FIDDLE);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.FS_OPEN_TEMPLATE);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.FS_SAVE_FIDDLE);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.FS_SAVE_FIDDLE_FORGE);
|
||||
|
||||
ipcRendererManager.on(IpcEvents.FS_OPEN_FIDDLE, (_event, filePath) => {
|
||||
this.openFiddle(filePath);
|
||||
});
|
||||
@@ -35,13 +39,19 @@ export class FileManager {
|
||||
});
|
||||
}
|
||||
|
||||
public async openTemplate(name: string) {
|
||||
const values = await getTemplateValues(name);
|
||||
this.setFiddle(values);
|
||||
/**
|
||||
* Opens a template.
|
||||
*
|
||||
* @param {string} templateName
|
||||
* @memberof FileManager
|
||||
*/
|
||||
public async openTemplate(templateName: string) {
|
||||
const editorValues = await getTemplateValues(templateName);
|
||||
await window.ElectronFiddle.app.replaceFiddle(editorValues, {templateName});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to open a fiddle
|
||||
* Tries to open a fiddle.
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @memberof FileManager
|
||||
@@ -53,33 +63,16 @@ export class FileManager {
|
||||
|
||||
// We'll do our best and will likely have to
|
||||
// rewrite this once we support multiple files
|
||||
const values: EditorValues = {
|
||||
const editorValues: EditorValues = {
|
||||
html: await this.readFile(path.join(filePath, INDEX_HTML_NAME)),
|
||||
main: await this.readFile(path.join(filePath, MAIN_JS_NAME)),
|
||||
renderer: await this.readFile(path.join(filePath, RENDERER_JS_NAME)),
|
||||
preload: await this.readFile(path.join(filePath, PRELOAD_JS_NAME)),
|
||||
css: await this.readFile(path.join(filePath, STYLES_CSS_NAME))
|
||||
};
|
||||
|
||||
this.setFiddle(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Got values? This method will load them.
|
||||
*
|
||||
* @param {EditorValues} values
|
||||
* @memberof FileManager
|
||||
*/
|
||||
public async setFiddle(values: EditorValues, filePath?: string) {
|
||||
this.appState.gistId = '';
|
||||
this.appState.isMyGist = false;
|
||||
this.appState.localPath = filePath || null;
|
||||
|
||||
this.appState.setWarningDialogTexts({
|
||||
label: `Opening this fiddle will replace your unsaved changes. Do you want to proceed?`,
|
||||
ok: 'Yes'
|
||||
});
|
||||
|
||||
await window.ElectronFiddle.app.setValues(values);
|
||||
document.title = getTitle(this.appState);
|
||||
window.ElectronFiddle.app.replaceFiddle(editorValues, {filePath});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +82,7 @@ export class FileManager {
|
||||
* @param {string} filePath
|
||||
* @memberof FileManager
|
||||
*/
|
||||
public async saveFiddle(filePath: string, ...transforms: Array<FileTransform>) {
|
||||
public async saveFiddle(filePath?: string, ...transforms: Array<FileTransform>) {
|
||||
const { localPath } = this.appState;
|
||||
const pathToSave = filePath || localPath;
|
||||
|
||||
@@ -101,10 +94,14 @@ export class FileManager {
|
||||
const files = await this.getFiles(undefined, ...transforms);
|
||||
|
||||
for (const [fileName, content] of files) {
|
||||
try {
|
||||
await this.saveFile(path.join(pathToSave, fileName), content);
|
||||
} catch (error) {
|
||||
console.warn(`FileManager: Failed to save file`, { fileName, error });
|
||||
const savePath = path.join(pathToSave, fileName);
|
||||
|
||||
// If the file has content, save it to disk. If there's no
|
||||
// content in the file, remove a file that possibly exists.
|
||||
if (content) {
|
||||
await this.saveFile(savePath, content);
|
||||
} else {
|
||||
await this.removeFile(savePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +110,6 @@ export class FileManager {
|
||||
}
|
||||
|
||||
this.appState.isUnsaved = false;
|
||||
window.ElectronFiddle.app.setupUnsavedOnChangeListener();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +124,14 @@ export class FileManager {
|
||||
*/
|
||||
public async getFiles(options?: PackageJsonOptions, ...transforms: Array<FileTransform>): Promise<Files> {
|
||||
const pOptions = typeof options === 'object' ? options : DEFAULT_OPTIONS;
|
||||
const values = await window.ElectronFiddle.app.getValues(pOptions);
|
||||
const values = await window.ElectronFiddle.app.getEditorValues(pOptions);
|
||||
let output: Files = new Map();
|
||||
|
||||
output.set(RENDERER_JS_NAME, values.renderer);
|
||||
output.set(MAIN_JS_NAME, values.main);
|
||||
output.set(INDEX_HTML_NAME, values.html);
|
||||
output.set(PRELOAD_JS_NAME, values.preload);
|
||||
output.set(STYLES_CSS_NAME, values.css);
|
||||
output.set(PACKAGE_NAME, values.package!);
|
||||
|
||||
for (const transform of transforms) {
|
||||
@@ -236,4 +234,22 @@ export class FileManager {
|
||||
ipcRendererManager.send(IpcEvents.FS_SAVE_FIDDLE_ERROR, [filePath]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely attempts to remove a file, doesn't crash the app if
|
||||
* it fails.
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @returns {string}
|
||||
* @memberof FileManager
|
||||
*/
|
||||
private async removeFile(filePath: string): Promise<void> {
|
||||
try {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
return await fs.remove(filePath);
|
||||
} catch (error) {
|
||||
console.log(`FileManager: Could not remove ${filePath}`, error);
|
||||
ipcRendererManager.send(IpcEvents.FS_SAVE_FIDDLE_ERROR, [filePath]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export class IpcRendererManager extends EventEmitter {
|
||||
super();
|
||||
|
||||
ipcRendererEvents.forEach((name) => {
|
||||
ipcRenderer.removeAllListeners(name);
|
||||
ipcRenderer.on(name, (...args: Array<any>) => this.emit(name, ...args));
|
||||
});
|
||||
}
|
||||
|
||||
+10
-5
@@ -1,6 +1,8 @@
|
||||
import { EditorValues } from '../interfaces';
|
||||
import { exec } from '../utils/exec';
|
||||
|
||||
const { builtinModules } = require('module');
|
||||
|
||||
export interface NpmOperationOptions {
|
||||
dir: string;
|
||||
}
|
||||
@@ -11,12 +13,12 @@ export let isInstalled: boolean | null = null;
|
||||
/* perhaps we can expose this to the settings module?*/
|
||||
const ignoredModules: Array<string> = [
|
||||
'electron',
|
||||
// tslint:disable-next-line:no-submodule-imports
|
||||
...require('builtin-modules/static')
|
||||
'original-fs',
|
||||
...builtinModules
|
||||
];
|
||||
|
||||
/* regular expression to both match and extract module names */
|
||||
const requiregx = /require\(['"](.*?)['"]\)/gm;
|
||||
const requiregx = /^.*require\(['"](.*?)['"]\)/gm;
|
||||
|
||||
|
||||
/*
|
||||
@@ -91,8 +93,11 @@ export function findModules(input: string): Array<string> {
|
||||
/* grab all global require matches in the text */
|
||||
// tslint:disable-next-line:no-conditional-assignment
|
||||
while (match = (requiregx.exec(input) || null)) {
|
||||
const mod = match[1];
|
||||
modules.push(mod);
|
||||
// ensure commented-out requires aren't downloaded
|
||||
if (!match[0].startsWith('//')) {
|
||||
const mod = match[1];
|
||||
modules.push(mod);
|
||||
}
|
||||
}
|
||||
|
||||
/* map and reduce */
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { when } from 'mobx';
|
||||
import { EditorId, EditorValues, GenericDialogType } from '../interfaces';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
|
||||
import { getOctokit } from '../utils/octokit';
|
||||
import { sortedElectronMap } from '../utils/sorted-electron-map';
|
||||
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
|
||||
import { getContent } from './content';
|
||||
import { AppState } from './state';
|
||||
import { ElectronReleaseChannel, getReleaseChannel } from './versions';
|
||||
|
||||
export class RemoteLoader {
|
||||
constructor(private readonly appState: AppState) {
|
||||
this.loadFiddleFromElectronExample.bind(this);
|
||||
this.loadFiddleFromGist.bind(this);
|
||||
this.verifyRemoteLoad.bind(this);
|
||||
this.verifyReleaseChannelEnabled.bind(this);
|
||||
this.fetchExampleAndLoad.bind(this);
|
||||
this.fetchGistAndLoad.bind(this);
|
||||
this.setElectronVersionWithRef.bind(this);
|
||||
this.getPackageVersionFromRef.bind(this);
|
||||
this.handleLoadingSuccess.bind(this);
|
||||
this.handleLoadingFailed.bind(this);
|
||||
}
|
||||
|
||||
public async loadFiddleFromElectronExample(_: any, exampleInfo: { path: string; ref: string }) {
|
||||
console.log(`Loading fiddle from Electron example`, _, exampleInfo);
|
||||
const { path, ref } = exampleInfo;
|
||||
const prettyName = path.replace('docs/fiddles/', '');
|
||||
const ok = await this.verifyRemoteLoad(`'${prettyName}' example from the Electron docs for version ${ref}`);
|
||||
if (!ok) return;
|
||||
|
||||
this.fetchExampleAndLoad(ref, path);
|
||||
}
|
||||
|
||||
public async loadFiddleFromGist(_: any, gistInfo: { id: string }) {
|
||||
const { id } = gistInfo;
|
||||
const ok = await this.verifyRemoteLoad(`gist`);
|
||||
if (!ok) return;
|
||||
|
||||
this.fetchGistAndLoad(id);
|
||||
}
|
||||
|
||||
public async fetchExampleAndLoad(ref: string, path: string): Promise<boolean> {
|
||||
try {
|
||||
const octo = await getOctokit(this.appState);
|
||||
|
||||
const folder = await octo.repos.getContents({
|
||||
owner: ELECTRON_REPO,
|
||||
repo: ELECTRON_ORG,
|
||||
ref,
|
||||
path,
|
||||
});
|
||||
|
||||
const ok = await this.setElectronVersionWithRef(ref);
|
||||
if (!ok) return false;
|
||||
|
||||
const values = {
|
||||
html: await getContent(EditorId.html, this.appState.version),
|
||||
renderer: await getContent(EditorId.renderer, this.appState.version),
|
||||
main: await getContent(EditorId.main, this.appState.version),
|
||||
preload: await getContent(EditorId.preload, this.appState.version),
|
||||
css: await getContent(EditorId.css, this.appState.version)
|
||||
};
|
||||
|
||||
const loaders: Array<Promise<void>> = [];
|
||||
if (!Array.isArray(folder.data)) {
|
||||
throw new Error('The example Fiddle tried to launch is not a valid Electron example');
|
||||
}
|
||||
|
||||
for (const child of folder.data) {
|
||||
if (!child.download_url) {
|
||||
console.warn(`Could not find download_url for ${child.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (child.name) {
|
||||
case MAIN_JS_NAME:
|
||||
loaders.push(fetch(child.download_url)
|
||||
.then((r) => r.text()).then((t) => { values.main = t; })
|
||||
);
|
||||
|
||||
break;
|
||||
case INDEX_HTML_NAME:
|
||||
loaders.push(fetch(child.download_url)
|
||||
.then((r) => r.text()).then((t) => { values.html = t; })
|
||||
);
|
||||
|
||||
break;
|
||||
case RENDERER_JS_NAME:
|
||||
loaders.push(fetch(child.download_url)
|
||||
.then((r) => r.text()).then((t) => { values.renderer = t; })
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case PRELOAD_JS_NAME:
|
||||
loaders.push(fetch(child.download_url)
|
||||
.then((r) => r.text()).then((t) => { values.preload = t; })
|
||||
);
|
||||
|
||||
case STYLES_CSS_NAME:
|
||||
loaders.push(fetch(child.download_url)
|
||||
.then((r) => r.text()).then((t) => { values.css = t; })
|
||||
);
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(loaders);
|
||||
|
||||
return this.handleLoadingSuccess(values, '');
|
||||
} catch (error) {
|
||||
return this.handleLoadingFailed(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from a gist. If it doesn't exist, return an empty string.
|
||||
*
|
||||
* @param {Octokit.Response<Octokit.GistsGetResponse>} gist
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
* @memberof RemoteLoader
|
||||
*/
|
||||
public getContentOrEmpty(gist: Octokit.Response<Octokit.GistsGetResponse>, name: string): string {
|
||||
try {
|
||||
return gist.data.files[name].content;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a fiddle
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* @memberof RemoteLoader
|
||||
*/
|
||||
public async fetchGistAndLoad(gistId: string): Promise<boolean> {
|
||||
try {
|
||||
const octo = await getOctokit(this.appState);
|
||||
const gist = await octo.gists.get({ gist_id: gistId });
|
||||
|
||||
return this.handleLoadingSuccess({
|
||||
html: this.getContentOrEmpty(gist, INDEX_HTML_NAME),
|
||||
main: this.getContentOrEmpty(gist, MAIN_JS_NAME),
|
||||
renderer: this.getContentOrEmpty(gist, RENDERER_JS_NAME),
|
||||
preload: this.getContentOrEmpty(gist, PRELOAD_JS_NAME),
|
||||
css: this.getContentOrEmpty(gist, STYLES_CSS_NAME)
|
||||
}, gistId);
|
||||
} catch (error) {
|
||||
return this.handleLoadingFailed(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async setElectronVersionWithRef(ref: string): Promise<boolean> {
|
||||
const version = await this.getPackageVersionFromRef(ref);
|
||||
|
||||
const supportedVersions = sortedElectronMap(this.appState.versions, (k) => k);
|
||||
if (!supportedVersions.includes(version)) {
|
||||
this.handleLoadingFailed(new Error('Version of Electron in example not supported'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if version is part of release channel
|
||||
const versionReleaseChannel: ElectronReleaseChannel = getReleaseChannel(version);
|
||||
|
||||
if (!this.appState.channelsToShow.includes(versionReleaseChannel)) {
|
||||
const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
|
||||
if (!ok) return false;
|
||||
|
||||
this.appState.channelsToShow.push(versionReleaseChannel);
|
||||
}
|
||||
|
||||
this.appState.setVersion(version);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async getPackageVersionFromRef(ref: string): Promise<string> {
|
||||
const octo = await getOctokit(this.appState);
|
||||
const { data: packageJsonData } = await octo.repos.getContents({
|
||||
owner: ELECTRON_ORG,
|
||||
repo: ELECTRON_REPO,
|
||||
ref,
|
||||
path: 'package.json'
|
||||
});
|
||||
|
||||
if (!Array.isArray(packageJsonData) && !!packageJsonData.content) {
|
||||
const packageJsonString = Buffer.from(packageJsonData.content, 'base64').toString('utf8');
|
||||
const { version } = JSON.parse(packageJsonString);
|
||||
return version;
|
||||
} else {
|
||||
console.error(`getPackageVersionFromRef: Received unexpected response from GitHub, could not parse version`, {
|
||||
packageJsonData
|
||||
});
|
||||
|
||||
return '0.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies from the user that we should be loading this fiddle
|
||||
*
|
||||
* @param what What are we loading from (gist, example, etc.)
|
||||
*/
|
||||
public async verifyRemoteLoad(what: string): Promise<boolean> {
|
||||
this.appState.setGenericDialogOptions({
|
||||
type: GenericDialogType.confirm,
|
||||
label: `Are you sure you want to load this ${what}? Only load and run it if you trust the source.`
|
||||
});
|
||||
this.appState.isGenericDialogShowing = true;
|
||||
await when(() => !this.appState.isGenericDialogShowing);
|
||||
|
||||
return !!this.appState.genericDialogLastResult;
|
||||
}
|
||||
|
||||
public async verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
|
||||
this.appState.setGenericDialogOptions({
|
||||
type: GenericDialogType.warning,
|
||||
label: `You're loading an example with a version of Electron with an unincluded release
|
||||
channel (${channel}). Do you want to enable the release channel to load the
|
||||
version of Electron from the example?`
|
||||
});
|
||||
this.appState.isGenericDialogShowing = true;
|
||||
await when(() => !this.appState.isGenericDialogShowing);
|
||||
|
||||
return !!this.appState.genericDialogLastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading a fiddle from GitHub succeeded, let's move on.
|
||||
*
|
||||
* @param {Partial<EditorValues>} values
|
||||
* @param {string} gistId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private async handleLoadingSuccess(values: Partial<EditorValues>, gistId: string): Promise<boolean> {
|
||||
await window.ElectronFiddle.app.replaceFiddle(values, { gistId });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading a fiddle from GitHub failed - this method handles this case
|
||||
* gracefully.
|
||||
*
|
||||
* @param {Error} error
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private handleLoadingFailed(error: Error): false {
|
||||
if (navigator.onLine) {
|
||||
this.appState.setGenericDialogOptions({
|
||||
type: GenericDialogType.warning,
|
||||
label: `Loading the fiddle failed: ${error}`,
|
||||
cancel: undefined
|
||||
});
|
||||
} else {
|
||||
this.appState.setGenericDialogOptions({
|
||||
type: GenericDialogType.warning,
|
||||
label: `Loading the fiddle failed. Your computer seems to be offline. Error: ${error}`,
|
||||
cancel: undefined
|
||||
});
|
||||
}
|
||||
|
||||
this.appState.toggleGenericDialog();
|
||||
|
||||
console.warn(`Loading Fiddle failed`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import * as path from 'path';
|
||||
|
||||
import { EditorValues, FileTransform } from '../interfaces';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { PackageJsonOptions } from '../utils/get-package';
|
||||
import { maybePlural } from '../utils/plural-maybe';
|
||||
import { getElectronBinaryPath, getIsDownloaded } from './binary';
|
||||
import { ipcRendererManager } from './ipc';
|
||||
import { findModulesInEditors, getIsNpmInstalled, installModules, npmRun } from './npm';
|
||||
import { AppState } from './state';
|
||||
|
||||
export enum ForgeCommands {
|
||||
PACKAGE = 'package',
|
||||
MAKE = 'make'
|
||||
}
|
||||
|
||||
export class Runner {
|
||||
public child: ChildProcess | null = null;
|
||||
|
||||
constructor(private readonly appState: AppState) {
|
||||
this.run = this.run.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_RUN);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_PACKAGE);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_MAKE);
|
||||
|
||||
|
||||
ipcRendererManager.on(IpcEvents.FIDDLE_RUN, this.run);
|
||||
ipcRendererManager.on(IpcEvents.FIDDLE_PACKAGE, () => {
|
||||
this.performForgeOperation(ForgeCommands.PACKAGE);
|
||||
});
|
||||
ipcRendererManager.on(IpcEvents.FIDDLE_MAKE, () => {
|
||||
this.performForgeOperation(ForgeCommands.MAKE);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually run the fiddle.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public async run(): Promise<boolean> {
|
||||
const { fileManager, getEditorValues } = window.ElectronFiddle.app;
|
||||
const options = { includeDependencies: false, includeElectron: false };
|
||||
const { currentElectronVersion } = this.appState;
|
||||
const { version, localPath } = currentElectronVersion;
|
||||
|
||||
if (this.appState.isClearingConsoleOnRun) {
|
||||
this.appState.clearConsole();
|
||||
}
|
||||
this.appState.isConsoleShowing = true;
|
||||
|
||||
const values = await getEditorValues(options);
|
||||
const dir = await this.saveToTemp(options);
|
||||
|
||||
if (!dir) return false;
|
||||
|
||||
try {
|
||||
await this.installModulesForEditor(values, dir);
|
||||
} catch (error) {
|
||||
console.error('Runner: Could not install modules', error);
|
||||
fileManager.cleanup(dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isReady = await getIsDownloaded(version, localPath);
|
||||
|
||||
if (!isReady) {
|
||||
console.warn(`Runner: Binary ${version} not ready`);
|
||||
|
||||
let message = `Could not start fiddle: `;
|
||||
message += `Electron ${version} not downloaded yet. `;
|
||||
message += `Please wait for it to finish downloading `;
|
||||
message += `before running the fiddle.`;
|
||||
|
||||
this.appState.pushOutput(message, { isNotPre: true });
|
||||
fileManager.cleanup(dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.execute(dir);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a currently running Electron fiddle.
|
||||
*/
|
||||
public async stop() {
|
||||
if (this.child) {
|
||||
this.child.kill();
|
||||
this.appState.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses electron-forge to either package or make the current fiddle
|
||||
*
|
||||
* @param {ForgeCommands} operation
|
||||
* @returns {Promise<boolean>}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async performForgeOperation(operation: ForgeCommands): Promise<boolean> {
|
||||
const options = { includeDependencies: true, includeElectron: true };
|
||||
const { dotfilesTransform } = await import('./transforms/dotfiles');
|
||||
const { forgeTransform } = await import('./transforms/forge');
|
||||
const { pushError, pushOutput } = this.appState;
|
||||
|
||||
const strings = operation === ForgeCommands.MAKE
|
||||
? [ 'Creating installers for', 'Binary' ]
|
||||
: [ 'Packaging', 'Installers' ];
|
||||
|
||||
this.appState.isConsoleShowing = true;
|
||||
pushOutput(`📦 ${strings[0]} current Fiddle...`);
|
||||
|
||||
if (!(await getIsNpmInstalled())) {
|
||||
let message = `Error: Could not find npm. Fiddle requires Node.js and npm `;
|
||||
message += `to compile packages. Please visit https://nodejs.org to install `;
|
||||
message += `Node.js and npm.`;
|
||||
|
||||
this.appState.pushOutput(message, { isNotPre: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save files to temp
|
||||
const dir = await this.saveToTemp(options, dotfilesTransform, forgeTransform);
|
||||
if (!dir) return false;
|
||||
|
||||
// Files are now saved to temp, let's install Forge and dependencies
|
||||
if (!(await this.npmInstall(dir))) return false;
|
||||
|
||||
// Cool, let's run "package"
|
||||
try {
|
||||
console.log(`Now creating ${strings[1].toLowerCase()}...`);
|
||||
pushOutput(await npmRun({ dir }, operation));
|
||||
pushOutput(`✅ ${strings[1]} successfully created.`, { isNotPre: true });
|
||||
} catch (error) {
|
||||
pushError(`Creating ${strings[1].toLowerCase()} failed.`, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { shell } = await import('electron');
|
||||
shell.showItemInFolder(path.join(dir, 'out'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the editor's JavaScript contents for modules
|
||||
* and installs them.
|
||||
*
|
||||
* @param {EditorValues} values
|
||||
* @param {string} dir
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async installModulesForEditor(values: EditorValues, dir: string): Promise<void> {
|
||||
const modules = await findModulesInEditors(values);
|
||||
const { pushOutput } = this.appState;
|
||||
|
||||
if (modules && modules.length > 0) {
|
||||
if (!(await getIsNpmInstalled())) {
|
||||
let message = `The ${maybePlural(`module`, modules)} ${modules.join(', ')} need to be installed, `;
|
||||
message += `but we could not find npm. Fiddle requires Node.js and npm `;
|
||||
message += `to support the installation of modules not included in `;
|
||||
message += `Electron. Please visit https://nodejs.org to install Node.js `;
|
||||
message += `and npm.`;
|
||||
|
||||
pushOutput(message, { isNotPre: true });
|
||||
return;
|
||||
}
|
||||
|
||||
pushOutput(`Installing npm modules: ${modules.join(', ')}...`, { isNotPre: true });
|
||||
pushOutput(await installModules({ dir }, ...modules));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Electron.
|
||||
*
|
||||
* @param {string} dir
|
||||
* @param {string} version
|
||||
* @returns {Promise<void>}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async execute(dir: string): Promise<void> {
|
||||
const { currentElectronVersion, pushOutput } = this.appState;
|
||||
const { version, localPath } = currentElectronVersion;
|
||||
const binaryPath = getElectronBinaryPath(version, localPath);
|
||||
console.log(`Runner: Binary ${binaryPath} ready, launching`);
|
||||
|
||||
const env = { ...process.env };
|
||||
if (this.appState.isEnablingElectronLogging) {
|
||||
env.ELECTRON_ENABLE_LOGGING = 'true';
|
||||
env.ELECTRON_DEBUG_NOTIFICATIONS = 'true';
|
||||
env.ELECTRON_ENABLE_STACK_DUMPING = 'true';
|
||||
} else {
|
||||
delete env.ELECTRON_ENABLE_LOGGING;
|
||||
delete env.ELECTRON_DEBUG_NOTIFICATIONS;
|
||||
delete env.ELECTRON_ENABLE_STACK_DUMPING;
|
||||
}
|
||||
|
||||
// Add user-specified cli flags if any have been set.
|
||||
const options = [ dir, '--inspect' ].concat(this.appState.executionFlags);
|
||||
|
||||
this.child = spawn(binaryPath, options, { cwd: dir, env });
|
||||
this.appState.isRunning = true;
|
||||
pushOutput(`Electron v${version} started.`);
|
||||
|
||||
this.child.stdout!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
|
||||
this.child.stderr!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
|
||||
this.child.on('close', async (code) => {
|
||||
const withCode = typeof code === 'number'
|
||||
? ` with code ${code.toString()}.`
|
||||
: `.`;
|
||||
|
||||
pushOutput(`Electron exited${withCode}`);
|
||||
this.appState.isRunning = false;
|
||||
this.child = null;
|
||||
|
||||
// Clean older folders
|
||||
await window.ElectronFiddle.app.fileManager.cleanup(dir);
|
||||
await this.deleteUserData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save files to temp, logging to the Fiddle terminal while doing so
|
||||
*
|
||||
* @param {PackageJsonOptions} options
|
||||
* @param {...Array<FileTransform>} transforms
|
||||
* @returns {(Promise<string | null>)}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async saveToTemp(
|
||||
options: PackageJsonOptions, ...transforms: Array<FileTransform>
|
||||
): Promise<string | null> {
|
||||
const { fileManager } = window.ElectronFiddle.app;
|
||||
const { pushOutput, pushError } = this.appState;
|
||||
|
||||
try {
|
||||
pushOutput(`Saving files to temp directory...`);
|
||||
const dir = await fileManager.saveToTemp(options, ...transforms);
|
||||
pushOutput(`Saved files to ${dir}`);
|
||||
return dir;
|
||||
} catch (error) {
|
||||
pushError('Failed to save files.', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs modules in a given directory (we're basically
|
||||
* just running "npm install")
|
||||
*
|
||||
* @param {string} dir
|
||||
* @returns
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async npmInstall(dir: string): Promise<boolean> {
|
||||
try {
|
||||
this.appState.pushOutput(`Now running "npm install..."`);
|
||||
this.appState.pushOutput(await installModules({ dir }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.appState.pushError('Failed to run "npm install".', error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the user data dir after a run.
|
||||
*/
|
||||
private async deleteUserData() {
|
||||
if (this.appState.isKeepingUserDataDirs) {
|
||||
console.log(`Cleanup: Not deleting data dir due to isKeepingUserDataDirs setting`);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = await this.appState.getName();
|
||||
const appData = path.join(this.appState.appData, name);
|
||||
|
||||
console.log(`Cleanup: Deleting data dir ${appData}`);
|
||||
await window.ElectronFiddle.app.fileManager.cleanup(appData);
|
||||
}
|
||||
}
|
||||
+340
-91
@@ -1,41 +1,58 @@
|
||||
import { action, autorun, observable, when } from 'mobx';
|
||||
import * as fsType from 'fs-extra';
|
||||
import { action, autorun, computed, observable, when } from 'mobx';
|
||||
import { MosaicNode } from 'react-mosaic-component';
|
||||
|
||||
import {
|
||||
ElectronVersion,
|
||||
ElectronVersionSource,
|
||||
ElectronVersionState,
|
||||
NpmVersion,
|
||||
ALL_MOSAICS,
|
||||
DocsDemoPage,
|
||||
EditorId,
|
||||
GenericDialogOptions,
|
||||
GenericDialogType,
|
||||
MosaicId,
|
||||
OutputEntry,
|
||||
OutputOptions,
|
||||
WarningDialogTexts
|
||||
RunnableVersion,
|
||||
Version,
|
||||
VersionSource,
|
||||
VersionState
|
||||
} from '../interfaces';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { arrayToStringMap } from '../utils/array-to-stringmap';
|
||||
import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
|
||||
import { createMosaicArrangement, getVisibleMosaics } from '../utils/editors-mosaic-arrangement';
|
||||
import { getName } from '../utils/get-title';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { normalizeVersion } from '../utils/normalize-version';
|
||||
import { BinaryManager } from './binary';
|
||||
import { ContentNames, getContent, isContentUnchanged } from './content';
|
||||
import { updateEditorTypeDefinitions } from './fetch-types';
|
||||
import { isEditorBackup, isEditorId, isPanelId } from '../utils/type-checks';
|
||||
import { getDownloadedVersions, getDownloadingVersions, removeBinary, setupBinary } from './binary';
|
||||
import { Bisector } from './bisect';
|
||||
import { DEFAULT_CLOSED_PANELS, DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
|
||||
import { getContent, isContentUnchanged } from './content';
|
||||
import { getLocalTypePathForVersion, updateEditorTypeDefinitions } from './fetch-types';
|
||||
import { ipcRendererManager } from './ipc';
|
||||
import { activateTheme } from './themes';
|
||||
|
||||
import { waitForEditorsToMount } from '../utils/editor-mounted';
|
||||
import { sortedElectronMap } from '../utils/sorted-electron-map';
|
||||
import {
|
||||
addLocalVersion,
|
||||
ElectronReleaseChannel,
|
||||
getDefaultVersion,
|
||||
getElectronVersions,
|
||||
getReleaseChannel,
|
||||
getUpdatedElectronVersions,
|
||||
saveLocalVersions
|
||||
} from './versions';
|
||||
|
||||
const knownVersions = getElectronVersions();
|
||||
const defaultVersion = localStorage.getItem('version')
|
||||
|| normalizeVersion(knownVersions[0].version);
|
||||
const defaultVersion = getDefaultVersion(knownVersions);
|
||||
|
||||
/**
|
||||
* Editors exist outside of React's world. To make things *a lot*
|
||||
* easier, we keep them around in a global object. Don't judge us,
|
||||
* we're really only doing that for the editors.
|
||||
*/
|
||||
window.ElectronFiddle = {
|
||||
window.ElectronFiddle = window.ElectronFiddle || {
|
||||
editors: {
|
||||
main: null,
|
||||
renderer: null,
|
||||
@@ -51,41 +68,63 @@ window.ElectronFiddle = {
|
||||
* @class AppState
|
||||
*/
|
||||
export class AppState {
|
||||
// Persisted settings
|
||||
// -- Persisted settings ------------------
|
||||
@observable public version: string = defaultVersion;
|
||||
@observable public theme: string | null = localStorage.getItem('theme');
|
||||
@observable public gitHubAvatarUrl: string | null = localStorage.getItem('gitHubAvatarUrl');
|
||||
@observable public gitHubName: string | null = localStorage.getItem('gitHubName');
|
||||
@observable public gitHubLogin: string | null = localStorage.getItem('gitHubLogin');
|
||||
@observable public gitHubToken: string | null = localStorage.getItem('gitHubToken') || null;
|
||||
@observable public gitHubPublishAsPublic: boolean = !!this.retrieve('gitHubPublishAsPublic', true);
|
||||
@observable public versionsToShow: Array<ElectronReleaseChannel> =
|
||||
this.retrieve('versionsToShow', true) as Array<ElectronReleaseChannel>
|
||||
|| [ ElectronReleaseChannel.stable, ElectronReleaseChannel.beta ];
|
||||
@observable public gitHubPublishAsPublic: boolean = !!this.retrieve('gitHubPublishAsPublic');
|
||||
@observable public channelsToShow: Array<ElectronReleaseChannel> =
|
||||
this.retrieve('channelsToShow') as Array<ElectronReleaseChannel>
|
||||
|| [ElectronReleaseChannel.stable, ElectronReleaseChannel.beta];
|
||||
@observable public statesToShow: Array<VersionState> =
|
||||
this.retrieve('statesToShow') as Array<VersionState>
|
||||
|| [VersionState.downloading, VersionState.ready, VersionState.unknown];
|
||||
@observable public isKeepingUserDataDirs: boolean = !!this.retrieve('isKeepingUserDataDirs');
|
||||
@observable public isEnablingElectronLogging: boolean = !!this.retrieve('isEnablingElectronLogging');
|
||||
@observable public isClearingConsoleOnRun: boolean = !!this.retrieve('isClearingConsoleOnRun');
|
||||
@observable public executionFlags: Array<string> =
|
||||
this.retrieve('executionFlags') as Array<string> === null ?
|
||||
[] : this.retrieve('executionFlags') as Array<string>;
|
||||
|
||||
@observable public binaryManager: BinaryManager = new BinaryManager();
|
||||
|
||||
// Various session-only state
|
||||
// -- Various session-only state ------------------
|
||||
@observable public gistId: string = '';
|
||||
@observable public isMyGist: boolean = false;
|
||||
@observable public versions: Record<string, ElectronVersion> = arrayToStringMap(knownVersions);
|
||||
@observable public versions: Record<string, RunnableVersion> = arrayToStringMap(knownVersions);
|
||||
@observable public output: Array<OutputEntry> = [];
|
||||
@observable public localPath: string | null = null;
|
||||
@observable public isUpdatingElectronVersions = false;
|
||||
@observable public warningDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
|
||||
@observable public warningDialogLastResult: boolean | null = null;
|
||||
@observable public localPath: string | undefined;
|
||||
@observable public genericDialogOptions = { type: GenericDialogType.warning, label: '', ok: 'Okay', cancel: 'Cancel' };
|
||||
@observable public genericDialogLastResult: boolean | null = null;
|
||||
@observable public mosaicArrangement: MosaicNode<MosaicId> | null = DEFAULT_MOSAIC_ARRANGEMENT;
|
||||
@observable public templateName: string | undefined;
|
||||
@observable public currentDocsDemoPage: DocsDemoPage = DocsDemoPage.DEFAULT;
|
||||
@observable public localTypeWatcher: fsType.FSWatcher | undefined;
|
||||
@observable public Bisector: Bisector | undefined;
|
||||
|
||||
// Various "isShowing" settings
|
||||
@observable public isPublishing: boolean = false;
|
||||
@observable public isRunning: boolean = false;
|
||||
@observable public isUnsaved: boolean;
|
||||
@observable public isUpdatingElectronVersions: boolean = false;
|
||||
@observable public isQuitting: boolean = false;
|
||||
|
||||
// -- Various "isShowing" settings ------------------
|
||||
@observable public isBisectCommandShowing: boolean;
|
||||
@observable public isConsoleShowing: boolean = false;
|
||||
@observable public isTokenDialogShowing: boolean = false;
|
||||
@observable public isWarningDialogShowing: boolean = false;
|
||||
@observable public isGenericDialogShowing: boolean = false;
|
||||
@observable public isSettingsShowing: boolean = false;
|
||||
@observable public isUnsaved: boolean = false;
|
||||
@observable public isBisectDialogShowing: boolean = false;
|
||||
@observable public isAddVersionDialogShowing: boolean = false;
|
||||
@observable public isThemeDialogShowing: boolean = false;
|
||||
@observable public isTourShowing: boolean = !localStorage.getItem('hasShownTour');
|
||||
|
||||
// -- Editor Values stored when we close the editor ------------------
|
||||
@observable public closedPanels: Partial<Record<MosaicId, EditorBackup | true>> = DEFAULT_CLOSED_PANELS;
|
||||
|
||||
private outputBuffer: string = '';
|
||||
private name: string;
|
||||
public appData: string;
|
||||
|
||||
constructor() {
|
||||
// Bind all actions
|
||||
@@ -96,57 +135,141 @@ export class AppState {
|
||||
this.setVersion = this.setVersion.bind(this);
|
||||
this.showTour = this.showTour.bind(this);
|
||||
this.signOutGitHub = this.signOutGitHub.bind(this);
|
||||
this.toggleBisectCommands = this.toggleBisectCommands.bind(this);
|
||||
this.toggleAuthDialog = this.toggleAuthDialog.bind(this);
|
||||
this.toggleConsole = this.toggleConsole.bind(this);
|
||||
this.clearConsole = this.clearConsole.bind(this);
|
||||
this.toggleSettings = this.toggleSettings.bind(this);
|
||||
this.toggleBisectDialog = this.toggleBisectDialog.bind(this);
|
||||
this.updateElectronVersions = this.updateElectronVersions.bind(this);
|
||||
this.resetEditorLayout = this.resetEditorLayout.bind(this);
|
||||
this.setIsQuitting = this.setIsQuitting.bind(this);
|
||||
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.OPEN_SETTINGS);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.SHOW_WELCOME_TOUR);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.CLEAR_CONSOLE);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.BISECT_COMMANDS_TOGGLE);
|
||||
|
||||
ipcRendererManager.on(IpcEvents.OPEN_SETTINGS, this.toggleSettings);
|
||||
ipcRendererManager.on(IpcEvents.SHOW_WELCOME_TOUR, this.showTour);
|
||||
ipcRendererManager.on(IpcEvents.CLEAR_CONSOLE, this.clearConsole);
|
||||
ipcRendererManager.on(IpcEvents.BISECT_COMMANDS_TOGGLE, this.toggleBisectCommands);
|
||||
ipcRendererManager.on(IpcEvents.BEFORE_QUIT, this.setIsQuitting);
|
||||
ipcRendererManager.once(IpcEvents.SET_APPDATA_DIR, (_event, dir) => { this.appData = dir; });
|
||||
|
||||
// Setup auto-runs
|
||||
autorun(() => this.save('theme', this.theme));
|
||||
autorun(() => this.save('isClearingConsoleOnRun', this.isClearingConsoleOnRun));
|
||||
autorun(() => this.save('gitHubAvatarUrl', this.gitHubAvatarUrl));
|
||||
autorun(() => this.save('gitHubLogin', this.gitHubLogin));
|
||||
autorun(() => this.save('gitHubName', this.gitHubName));
|
||||
autorun(() => this.save('gitHubToken', this.gitHubToken));
|
||||
autorun(() => this.save('gitHubPublishAsPublic', this.gitHubPublishAsPublic));
|
||||
autorun(() => this.save('isKeepingUserDataDirs', this.isKeepingUserDataDirs));
|
||||
autorun(() => this.save('isEnablingElectronLogging', this.isEnablingElectronLogging));
|
||||
autorun(() => this.save('executionFlags', this.executionFlags));
|
||||
autorun(() => this.save('version', this.version));
|
||||
autorun(() => this.save('versionsToShow', this.versionsToShow));
|
||||
autorun(() => this.save('channelsToShow', this.channelsToShow));
|
||||
autorun(() => this.save('statesToShow', this.statesToShow));
|
||||
|
||||
autorun(() => {
|
||||
if (this.isUnsaved) {
|
||||
if (typeof this.isUnsaved === 'undefined') return;
|
||||
|
||||
if (!!this.isUnsaved) {
|
||||
window.onbeforeunload = () => {
|
||||
this.setWarningDialogTexts({
|
||||
ipcRendererManager.send(IpcEvents.SHOW_INACTIVE);
|
||||
this.setGenericDialogOptions({
|
||||
type: GenericDialogType.warning,
|
||||
label: `The current Fiddle is unsaved. Do you want to exit anyway?`,
|
||||
ok: 'Quit'
|
||||
});
|
||||
|
||||
this.isWarningDialogShowing = true;
|
||||
this.isGenericDialogShowing = true;
|
||||
|
||||
// We'll wait until the warning dialog was closed
|
||||
when(() => !this.isWarningDialogShowing).then(() => {
|
||||
when(() => !this.isGenericDialogShowing).then(() => {
|
||||
const quitConfirmed = this.genericDialogLastResult;
|
||||
// The user confirmed, let's close for real.
|
||||
if (this.warningDialogLastResult) {
|
||||
if (quitConfirmed) {
|
||||
window.onbeforeunload = null;
|
||||
window.close();
|
||||
}
|
||||
|
||||
// isQuitting checks if we're trying to quit the app
|
||||
// or just close the window
|
||||
if (this.isQuitting) {
|
||||
ipcRendererManager.send(IpcEvents.CONFIRM_QUIT, quitConfirmed);
|
||||
this.isQuitting = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// return value doesn't matter, we just want to cancel the event
|
||||
return false;
|
||||
};
|
||||
} else {
|
||||
window.onbeforeunload = null;
|
||||
|
||||
// set up editor listeners to verify if unsaved
|
||||
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
|
||||
const editor = window.ElectronFiddle.editors[key];
|
||||
const disposable = editor.onDidChangeModelContent(() => {
|
||||
this.isUnsaved = true;
|
||||
disposable.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update our known versions
|
||||
this.updateElectronVersions();
|
||||
|
||||
// Make sure the console isn't all empty and sad
|
||||
this.pushOutput('Console ready 🔬');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current RunnableVersion or the first
|
||||
* one that can be found.
|
||||
*/
|
||||
@computed get currentElectronVersion(): RunnableVersion {
|
||||
if (this.versions[this.version]) {
|
||||
return this.versions[this.version];
|
||||
} else {
|
||||
return this.versions[defaultVersion];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of Electron versions to show given the
|
||||
* current settings for states and channels to display
|
||||
*/
|
||||
@computed get versionsToShow(): Array<RunnableVersion> {
|
||||
return sortedElectronMap<RunnableVersion>(this.versions, (_key, item) => item)
|
||||
.filter((item) => {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we want to show the version
|
||||
if (!this.channelsToShow.includes(getReleaseChannel(item))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we want to show the state
|
||||
if (!this.statesToShow.includes(item.state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the Electron versions: First, fetch them from GitHub,
|
||||
* then update their respective downloaded state.
|
||||
*
|
||||
* Fails silently.
|
||||
*/
|
||||
@action public async updateElectronVersions() {
|
||||
this.isUpdatingElectronVersions = true;
|
||||
@@ -156,7 +279,7 @@ export class AppState {
|
||||
this.versions = arrayToStringMap(versions);
|
||||
await this.updateDownloadedVersionState();
|
||||
} catch (error) {
|
||||
console.warn(`State: Could not update Electron versions`);
|
||||
console.warn(`State: Could not update Electron versions`, error);
|
||||
}
|
||||
|
||||
this.isUpdatingElectronVersions = false;
|
||||
@@ -174,22 +297,41 @@ export class AppState {
|
||||
this.isConsoleShowing = !this.isConsoleShowing;
|
||||
}
|
||||
|
||||
@action public clearConsole() {
|
||||
this.output = [];
|
||||
}
|
||||
|
||||
@action public toggleBisectCommands() {
|
||||
// guard against hiding the commands when executing a bisect
|
||||
if (!this.Bisector && !this.isBisectDialogShowing) {
|
||||
this.isBisectCommandShowing = !this.isBisectCommandShowing;
|
||||
}
|
||||
}
|
||||
|
||||
@action public toggleAddVersionDialog() {
|
||||
this.isAddVersionDialogShowing = !this.isAddVersionDialogShowing;
|
||||
}
|
||||
|
||||
@action public toggleAddMonacoThemeDialog() {
|
||||
this.isThemeDialogShowing = !this.isThemeDialogShowing;
|
||||
}
|
||||
|
||||
@action public toggleAuthDialog() {
|
||||
this.isTokenDialogShowing = !this.isTokenDialogShowing;
|
||||
}
|
||||
|
||||
@action public toogleWarningDialog() {
|
||||
this.isWarningDialogShowing = !this.isWarningDialogShowing;
|
||||
@action public toggleGenericDialog() {
|
||||
this.isGenericDialogShowing = !this.isGenericDialogShowing;
|
||||
|
||||
if (this.isWarningDialogShowing) {
|
||||
this.warningDialogLastResult = null;
|
||||
if (this.isGenericDialogShowing) {
|
||||
this.genericDialogLastResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
@action public toggleBisectDialog() {
|
||||
this.isBisectDialogShowing = !this.isBisectDialogShowing;
|
||||
}
|
||||
|
||||
@action public toggleSettings() {
|
||||
// We usually don't lose editor focus,
|
||||
// so you can still type. Let's force-blur.
|
||||
@@ -200,6 +342,10 @@ export class AppState {
|
||||
this.resetView({ isSettingsShowing: !this.isSettingsShowing });
|
||||
}
|
||||
|
||||
@action public setIsQuitting() {
|
||||
this.isQuitting = true;
|
||||
}
|
||||
|
||||
@action public disableTour() {
|
||||
this.resetView({ isTourShowing: false });
|
||||
localStorage.setItem('hasShownTour', 'true');
|
||||
@@ -215,27 +361,28 @@ export class AppState {
|
||||
window.ElectronFiddle.app.setupTheme();
|
||||
}
|
||||
|
||||
@action public setWarningDialogTexts(input: WarningDialogTexts) {
|
||||
this.warningDialogTexts = {
|
||||
@action public setGenericDialogOptions(opts: GenericDialogOptions) {
|
||||
this.genericDialogOptions = {
|
||||
type: GenericDialogType.warning,
|
||||
ok: 'Okay',
|
||||
cancel: 'Cancel',
|
||||
...input
|
||||
...opts
|
||||
};
|
||||
}
|
||||
|
||||
@action public async addLocalVersion(input: NpmVersion) {
|
||||
@action public addLocalVersion(input: Version) {
|
||||
addLocalVersion(input);
|
||||
|
||||
this.versions = arrayToStringMap(getElectronVersions());
|
||||
this.updateDownloadedVersionState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a version of Electron
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
/**
|
||||
* Remove a version of Electron
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@action public async removeVersion(input: string) {
|
||||
const version = normalizeVersion(input);
|
||||
const release = this.versions[version];
|
||||
@@ -252,7 +399,7 @@ export class AppState {
|
||||
const updatedVersions = { ...this.versions };
|
||||
|
||||
// Actually remove
|
||||
if (release && release.source === ElectronVersionSource.local) {
|
||||
if (release && release.source === VersionSource.local) {
|
||||
delete updatedVersions[version];
|
||||
|
||||
const versionsAsArray = Object
|
||||
@@ -261,49 +408,48 @@ export class AppState {
|
||||
|
||||
saveLocalVersions(versionsAsArray);
|
||||
} else {
|
||||
await this.binaryManager.remove(version);
|
||||
updatedVersions[version].state = ElectronVersionState.unknown;
|
||||
await removeBinary(version);
|
||||
updatedVersions[version].state = VersionState.unknown;
|
||||
}
|
||||
|
||||
this.versions = updatedVersions;
|
||||
this.updateDownloadedVersionState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a version of Electron.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
/**
|
||||
* Download a version of Electron.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@action public async downloadVersion(input: string) {
|
||||
const version = normalizeVersion(input);
|
||||
console.log(`State: Downloading Electron ${version}`);
|
||||
|
||||
const release = this.versions[version] || { state: '', source: '' };
|
||||
const isLocal = release.source === ElectronVersionSource.local;
|
||||
const isLocal = release.source === VersionSource.local;
|
||||
const isReady = release.state === 'ready';
|
||||
|
||||
// Fetch new binaries, maybe?
|
||||
if (!isLocal && !isReady) {
|
||||
console.log(`State: Instructing BinaryManager to fetch v${version}`);
|
||||
console.log(`State: Fetching v${version}`);
|
||||
const updatedVersions = { ...this.versions };
|
||||
updatedVersions[version] = updatedVersions[version] || {};
|
||||
updatedVersions[version].state = ElectronVersionState.downloading;
|
||||
this.versions = updatedVersions;
|
||||
|
||||
await this.binaryManager.setup(version);
|
||||
await setupBinary(this, version);
|
||||
this.updateDownloadedVersionState();
|
||||
} else {
|
||||
console.log(`State: Version ${version} already downloaded, doing nothing.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a version of Electron (and download it if necessary).
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
/**
|
||||
* Select a version of Electron (and download it if necessary).
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@action public async setVersion(input: string) {
|
||||
const version = normalizeVersion(input);
|
||||
|
||||
@@ -319,34 +465,64 @@ export class AppState {
|
||||
this.version = version;
|
||||
|
||||
// Should we update the editor?
|
||||
if (await isContentUnchanged(ContentNames.MAIN)) {
|
||||
const main = await getContent(ContentNames.MAIN, version);
|
||||
window.ElectronFiddle.app.setValues({ main }, false);
|
||||
if (await isContentUnchanged(EditorId.main)) {
|
||||
const main = await getContent(EditorId.main, version);
|
||||
window.ElectronFiddle.app.setEditorValues({ main });
|
||||
}
|
||||
|
||||
// Update TypeScript definitions
|
||||
updateEditorTypeDefinitions(version);
|
||||
const versionObject = this.versions[version];
|
||||
|
||||
if (versionObject.source === VersionSource.local) {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
const typePath = getLocalTypePathForVersion(versionObject);
|
||||
console.info(`TypeDefs: Watching file for local version ${version} at path ${typePath}`);
|
||||
try {
|
||||
this.localTypeWatcher = fs.watch(typePath!, async () => {
|
||||
console.info(`TypeDefs: Noticed file change at ${typePath}. Updating editor typedefs.`);
|
||||
await updateEditorTypeDefinitions(versionObject);
|
||||
});
|
||||
} catch (err) {
|
||||
console.info('TypeDefs: Unable to start watching.');
|
||||
}
|
||||
} else {
|
||||
if (!!this.localTypeWatcher) {
|
||||
console.info(`TypeDefs: Switched to downloaded version ${version}. Unwatching local typedefs.`);
|
||||
this.localTypeWatcher.close();
|
||||
this.localTypeWatcher = undefined;
|
||||
}
|
||||
}
|
||||
await updateEditorTypeDefinitions(versionObject);
|
||||
|
||||
// Fetch new binaries, maybe?
|
||||
await this.downloadVersion(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go and check which versions have already been downloaded.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
/**
|
||||
* Go and check which versions have already been downloaded.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@action public async updateDownloadedVersionState(): Promise<void> {
|
||||
const downloadedVersions = await this.binaryManager.getDownloadedVersions();
|
||||
const updatedVersions = { ...this.versions };
|
||||
|
||||
console.log(`State: Updating version state`);
|
||||
(downloadedVersions || []).forEach((version) => {
|
||||
// Keep state of currently downloading binaries first
|
||||
const downloadingVersions = getDownloadingVersions(this);
|
||||
(downloadingVersions || []).forEach((version) => {
|
||||
if (updatedVersions[version]) {
|
||||
updatedVersions[version].state = ElectronVersionState.ready;
|
||||
updatedVersions[version].state = VersionState.downloading;
|
||||
}
|
||||
});
|
||||
|
||||
const downloadedVersions = await getDownloadedVersions();
|
||||
(downloadedVersions || []).forEach((version) => {
|
||||
if (updatedVersions[version]) {
|
||||
updatedVersions[version].state = VersionState.ready;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`State: Updated version state`, updatedVersions);
|
||||
|
||||
this.versions = updatedVersions;
|
||||
}
|
||||
|
||||
@@ -374,12 +550,11 @@ export class AppState {
|
||||
let strData = data.toString();
|
||||
const { isNotPre, bypassBuffer } = options;
|
||||
|
||||
// Todo: This drops the first part of the buffer... is that fully expected?
|
||||
// TODO: This drops the first part of the buffer... is that fully expected?
|
||||
if (process.platform === 'win32' && bypassBuffer === false) {
|
||||
this.outputBuffer += strData;
|
||||
strData = this.outputBuffer;
|
||||
const parts = strData.split('\r\n');
|
||||
|
||||
for (let partIndex = 0; partIndex < parts.length; partIndex += 1) {
|
||||
const part = parts[partIndex];
|
||||
if (partIndex === parts.length - 1) {
|
||||
@@ -415,6 +590,84 @@ export class AppState {
|
||||
console.warn(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the editor value for a given editor. Deletes the value after
|
||||
* accessing it.
|
||||
*
|
||||
* @param {EditorId} id
|
||||
*/
|
||||
@action public getAndRemoveEditorValueBackup(id: EditorId): EditorBackup | null {
|
||||
const value = this.closedPanels[id];
|
||||
|
||||
if (isEditorBackup(value)) {
|
||||
delete this.closedPanels[id];
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@action public async setVisibleMosaics(visible: Array<MosaicId>) {
|
||||
const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
|
||||
|
||||
for (const id of ALL_MOSAICS) {
|
||||
if (!visible.includes(id) && currentlyVisible.includes(id)) {
|
||||
this.closedPanels[id] = isEditorId(id)
|
||||
? getEditorBackup(id)
|
||||
: true;
|
||||
|
||||
// if we have backup, remove active editor
|
||||
delete window.ElectronFiddle.editors[id];
|
||||
}
|
||||
|
||||
// Remove the backup for panels now. Editors will remove their
|
||||
// backup once the data has been loaded.
|
||||
if (isPanelId(id) && visible.includes(id) && !currentlyVisible.includes(id)) {
|
||||
delete this.closedPanels[id];
|
||||
}
|
||||
}
|
||||
|
||||
const updatedArrangement = createMosaicArrangement(visible);
|
||||
console.log(`State: Setting visible mosaic panels`, visible, updatedArrangement);
|
||||
|
||||
this.mosaicArrangement = updatedArrangement;
|
||||
|
||||
// after the mosaicArrangement loads, we want to wait for the Mosaic editors to
|
||||
// mount to ensure that we can load content into the editors as soon as they're
|
||||
// declared visible.
|
||||
|
||||
await waitForEditorsToMount(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the panel for a given MosaicId.
|
||||
*
|
||||
* @param {MosaicId} id
|
||||
*/
|
||||
@action public hideAndBackupMosaic(id: MosaicId) {
|
||||
const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
|
||||
this.setVisibleMosaics(currentlyVisible.filter((v) => v !== id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the editor value for a given editor.
|
||||
*
|
||||
* @param {MosaicId} id
|
||||
*/
|
||||
@action public showMosaic(id: MosaicId) {
|
||||
const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
|
||||
this.setVisibleMosaics([...currentlyVisible, id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets editor view to default layout
|
||||
*
|
||||
*
|
||||
*/
|
||||
@action public resetEditorLayout() {
|
||||
this.mosaicArrangement = DEFAULT_MOSAIC_ARRANGEMENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the view, optionally with certain view flags enabled.
|
||||
*
|
||||
@@ -427,6 +680,7 @@ export class AppState {
|
||||
this.isTourShowing = false;
|
||||
this.isConsoleShowing = false;
|
||||
this.isAddVersionDialogShowing = false;
|
||||
this.isThemeDialogShowing = false;
|
||||
|
||||
if (additionalOptions) {
|
||||
for (const key in additionalOptions) {
|
||||
@@ -479,17 +733,12 @@ export class AppState {
|
||||
*
|
||||
* @template T
|
||||
* @param {string} key
|
||||
* @param {boolean} parse
|
||||
* @returns {(T | string | null)}
|
||||
*/
|
||||
private retrieve<T>(key: string, parse: boolean): T | string | null {
|
||||
private retrieve<T>(key: string): T | string | null {
|
||||
const value = localStorage.getItem(key);
|
||||
|
||||
if (parse) {
|
||||
return JSON.parse(value || 'null') as T;
|
||||
}
|
||||
|
||||
return value;
|
||||
return JSON.parse(value || 'null') as T;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import * as fsExtraType from 'fs-extra';
|
||||
import * as pathType from 'path';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../constants';
|
||||
import { EditorValues } from '../interfaces';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
|
||||
import { fancyImport } from '../utils/import';
|
||||
|
||||
/**
|
||||
* Returns expected content for a given name. Currently synchronous,
|
||||
* but probably shouldn't be.
|
||||
* Returns expected content for a given name.
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {Promise<EditorValues>}
|
||||
@@ -15,8 +14,8 @@ import { fancyImport } from '../utils/import';
|
||||
export async function getTemplateValues(name: string): Promise<EditorValues> {
|
||||
const path = await fancyImport<typeof pathType>('path');
|
||||
const fs = await fancyImport<typeof fsExtraType>('fs-extra');
|
||||
|
||||
const templatePath = path.join(__dirname, '../../static/templates', name.toLowerCase());
|
||||
const templatesPath = path.join(__dirname, '../../static/show-me');
|
||||
const templatePath = path.join(templatesPath, name.toLowerCase());
|
||||
|
||||
const getFile = async (fileName: string) => {
|
||||
try {
|
||||
@@ -25,7 +24,15 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
|
||||
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.warn(`Could not get template file:`, error);
|
||||
console.warn(`getTemplateValues(): Could not get template file:`, error);
|
||||
|
||||
if (fs.existsSync(templatesPath)) {
|
||||
const contents = fs.readdirSync(templatesPath);
|
||||
console.log(`getTemplateValues(): ${templatesPath} contents:`, contents);
|
||||
} else {
|
||||
console.log(`getTemplateValues(): ${templatesPath} does not exist`);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
};
|
||||
@@ -33,6 +40,8 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
|
||||
return {
|
||||
renderer: await getFile(RENDERER_JS_NAME),
|
||||
main: await getFile(MAIN_JS_NAME),
|
||||
html: await getFile(INDEX_HTML_NAME)
|
||||
html: await getFile(INDEX_HTML_NAME),
|
||||
preload: await getFile(PRELOAD_JS_NAME),
|
||||
css: await getFile(STYLES_CSS_NAME)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as fsType from 'fs-extra';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as path from 'path';
|
||||
|
||||
import { CONFIG_PATH } from '../constants';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { CONFIG_PATH } from './constants';
|
||||
import { defaultDark, defaultLight, DefaultThemes, FiddleTheme, LoadedFiddleTheme } from './themes-defaults';
|
||||
|
||||
export const THEMES_PATH = path.join(CONFIG_PATH, 'themes');
|
||||
@@ -20,7 +20,7 @@ export async function activateTheme(
|
||||
const _monaco = monaco || window.ElectronFiddle.app.monaco;
|
||||
const _theme = theme || await getTheme(name);
|
||||
|
||||
if (!_monaco) return;
|
||||
if (!_monaco || !_monaco.editor) return;
|
||||
|
||||
_monaco.editor.defineTheme('main', _theme.editor as any);
|
||||
_monaco.editor.setTheme('main');
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import {
|
||||
remote,
|
||||
TouchBarButtonConstructorOptions,
|
||||
TouchBarScrubberConstructorOptions
|
||||
} from 'electron';
|
||||
import { autorun } from 'mobx';
|
||||
|
||||
import { RunnableVersion, VersionState } from '../interfaces';
|
||||
import { getNiceGreeting } from '../utils/nice-greeting';
|
||||
import { sortedElectronMap } from '../utils/sorted-electron-map';
|
||||
import { AppState } from './state';
|
||||
|
||||
const { TouchBar } = remote;
|
||||
const {
|
||||
TouchBarButton,
|
||||
TouchBarScrubber,
|
||||
TouchBarSpacer,
|
||||
TouchBarLabel
|
||||
} = TouchBar;
|
||||
|
||||
/**
|
||||
* Helper method: Returns an icon (emoji, lol) for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {Partial<RunnableVersion>} { state }
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getItemIcon(
|
||||
{ state }: Partial<RunnableVersion> = { state: VersionState.unknown }
|
||||
) {
|
||||
return state === 'ready'
|
||||
? '💾'
|
||||
: state === 'downloading' ? '⏬' : '☁';
|
||||
}
|
||||
|
||||
export class TouchBarManager {
|
||||
public browserWindow: Electron.BrowserWindow;
|
||||
public consoleBtn: Electron.TouchBarButton;
|
||||
public startStopBtn: Electron.TouchBarButton;
|
||||
public touchBar: Electron.TouchBar;
|
||||
public versionSelector: Electron.TouchBarScrubber;
|
||||
public versionSelectorBtn: Electron.TouchBarButton;
|
||||
public selectedVersion: string = '';
|
||||
public versionSelectorSelectBtn: Electron.TouchBarButton;
|
||||
// Lol have mercy, TS
|
||||
public items: Array<Electron.TouchBarButton
|
||||
| Electron.TouchBarColorPicker
|
||||
| Electron.TouchBarGroup
|
||||
| Electron.TouchBarLabel
|
||||
| Electron.TouchBarPopover
|
||||
| Electron.TouchBarScrubber
|
||||
| Electron.TouchBarSegmentedControl
|
||||
| Electron.TouchBarSlider
|
||||
| Electron.TouchBarSpacer>
|
||||
| undefined;
|
||||
|
||||
constructor(public readonly appState: AppState) {
|
||||
this.selectVersion = this.selectVersion.bind(this);
|
||||
this.updateStartStopBtn = this.updateStartStopBtn.bind(this);
|
||||
this.updateVersionSelectorItems = this.updateVersionSelectorItems.bind(this);
|
||||
this.updateVersionButton = this.updateVersionButton.bind(this);
|
||||
|
||||
this.startStopBtn = new TouchBarButton(this.getStartStopButtonOptions());
|
||||
this.consoleBtn = new TouchBarButton(this.getConsoleButtonOptions());
|
||||
this.versionSelectorBtn = new TouchBarButton(this.getVersionButtonOptions());
|
||||
this.versionSelectorSelectBtn = new TouchBarButton(this.getVersionSelectorSelectButtonOptions());
|
||||
this.versionSelector = new TouchBarScrubber(this.getVersionSelectorOptions());
|
||||
|
||||
this.setupTouchBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called right after we instantiate the manager. It sets up
|
||||
* autoruns that respond to state changes (like new versions) and also
|
||||
* greets the user with a kind little message.
|
||||
*/
|
||||
public setupTouchBar() {
|
||||
// First, the welcome
|
||||
this.setWelcomeItems();
|
||||
|
||||
// Then, go with the intro
|
||||
setTimeout(() => {
|
||||
this.setDefaultItems();
|
||||
}, 4000);
|
||||
|
||||
autorun(this.updateStartStopBtn);
|
||||
autorun(this.updateVersionSelectorItems);
|
||||
autorun(this.updateVersionButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever run/stop status changes, we'll update it (thanks, mobx)
|
||||
*/
|
||||
public updateStartStopBtn() {
|
||||
const { label } = this.getStartStopButtonOptions();
|
||||
this.startStopBtn.label = label!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever the electron versions change, we'll update
|
||||
* the items in the scrubber (thanks, mobx)
|
||||
*/
|
||||
public updateVersionSelectorItems() {
|
||||
this.versionSelector.items = this.getVersions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever the Electron version changes, update the version button
|
||||
*/
|
||||
public updateVersionButton() {
|
||||
const { label } = this.getVersionButtonOptions();
|
||||
this.versionSelectorBtn.label = label!;
|
||||
}
|
||||
|
||||
public getVersions() {
|
||||
return sortedElectronMap(this.appState.versions || {}, (key, version) => ({
|
||||
label: `${getItemIcon(version)} ${key}`,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely set the touch bar on the current window
|
||||
*
|
||||
* @param {Partial<Electron.TouchBarConstructorOptions>} options
|
||||
*/
|
||||
public setTouchBar(options: Partial<Electron.TouchBarConstructorOptions>) {
|
||||
try {
|
||||
this.items = options.items;
|
||||
this.touchBar = new TouchBar(options as Electron.TouchBarConstructorOptions);
|
||||
this.browserWindow = this.browserWindow || remote.getCurrentWindow();
|
||||
this.browserWindow.setTouchBar(this.touchBar);
|
||||
} catch (error) {
|
||||
console.warn(`Could not set touch bar`, { error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A bar that just contains a little welcome message
|
||||
*/
|
||||
public setWelcomeItems() {
|
||||
this.setTouchBar({
|
||||
items: [
|
||||
new TouchBarSpacer({ size: 'flexible' }),
|
||||
new TouchBarLabel({ label: getNiceGreeting() }),
|
||||
new TouchBarSpacer({ size: 'flexible' })
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A bar with the default items
|
||||
*/
|
||||
public setDefaultItems() {
|
||||
this.setTouchBar({
|
||||
items: [
|
||||
this.startStopBtn,
|
||||
this.consoleBtn,
|
||||
new TouchBarSpacer({ size: 'flexible' }),
|
||||
this.versionSelectorBtn,
|
||||
new TouchBarSpacer({ size: 'flexible' })
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A bar that just contains the version scrubber
|
||||
*/
|
||||
public setVersionSelectorItems() {
|
||||
this.setTouchBar({
|
||||
items: [
|
||||
this.versionSelector,
|
||||
this.versionSelectorSelectBtn,
|
||||
],
|
||||
escapeItem: new TouchBarButton(
|
||||
this.getVersionSelectorEscButtonOptions()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The version selector gets a custom "escape" button that'll
|
||||
* take the user back to the "default" touch bar interface
|
||||
*/
|
||||
public getVersionSelectorEscButtonOptions() {
|
||||
return {
|
||||
label: `Back`,
|
||||
click: () => {
|
||||
this.setDefaultItems();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The version selector also gets a "select" button that'll
|
||||
* commit the selected version
|
||||
*/
|
||||
public getVersionSelectorSelectButtonOptions() {
|
||||
return {
|
||||
label: `🎯 Select`,
|
||||
click: () => {
|
||||
if (this.selectedVersion) {
|
||||
this.appState.setVersion(this.selectedVersion);
|
||||
}
|
||||
|
||||
this.setDefaultItems();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The start button is wired up to launching the current
|
||||
* fiddle.
|
||||
*
|
||||
* @returns {TouchBarButtonConstructorOptions}
|
||||
*/
|
||||
public getStartStopButtonOptions(): TouchBarButtonConstructorOptions {
|
||||
const label = this.appState.isRunning
|
||||
? '🛑 Stop'
|
||||
: '🚀 Run';
|
||||
|
||||
const click = () => this.appState.isRunning
|
||||
? window.ElectronFiddle.app.runner.stop()
|
||||
: window.ElectronFiddle.app.runner.run();
|
||||
|
||||
return { label, click };
|
||||
}
|
||||
|
||||
/**
|
||||
* The console button is wired up to toggle the console
|
||||
*
|
||||
* @returns {TouchBarButtonConstructorOptions}
|
||||
*/
|
||||
public getConsoleButtonOptions(): TouchBarButtonConstructorOptions {
|
||||
const label = '🖥 Console';
|
||||
const click = this.appState.toggleConsole;
|
||||
|
||||
return { label, click };
|
||||
}
|
||||
|
||||
/**
|
||||
* The version button opens the scrubber, allowing selection
|
||||
* of a version
|
||||
*
|
||||
* @returns {TouchBarButtonConstructorOptions}
|
||||
*/
|
||||
public getVersionButtonOptions(): TouchBarButtonConstructorOptions {
|
||||
const label = `🔖 Electron ${this.appState.version}`;
|
||||
const click = () => this.setVersionSelectorItems();
|
||||
|
||||
return { label, click };
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the version selection scrubber
|
||||
*
|
||||
* @returns {TouchBarScrubberConstructorOptions}
|
||||
*/
|
||||
public getVersionSelectorOptions(
|
||||
): TouchBarScrubberConstructorOptions {
|
||||
return {
|
||||
select: this.selectVersion,
|
||||
highlight: this.selectVersion,
|
||||
items: [],
|
||||
selectedStyle: 'background',
|
||||
overlayStyle: 'outline',
|
||||
mode: 'free',
|
||||
continuous: true,
|
||||
showArrowButtons: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once the version selection scrubber is closed (
|
||||
* with confirmation)
|
||||
*
|
||||
* @param {number} index
|
||||
*/
|
||||
public selectVersion(index: number) {
|
||||
const versions = sortedElectronMap(this.appState.versions || {}, (k) => k);
|
||||
|
||||
this.selectedVersion = versions[index];
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PACKAGE_NAME } from '../../constants';
|
||||
import { Files } from '../../interfaces';
|
||||
import { PACKAGE_NAME } from '../../shared-constants';
|
||||
|
||||
/**
|
||||
* This transform turns the files into an electron-forge
|
||||
@@ -15,11 +15,11 @@ export async function forgeTransform(files: Files): Promise<Files> {
|
||||
|
||||
// devDependencies
|
||||
parsed.devDependencies = parsed.devDependencies || {};
|
||||
parsed.devDependencies['@electron-forge/cli'] = '6.0.0-beta.22';
|
||||
parsed.devDependencies['@electron-forge/maker-deb'] = '6.0.0-beta.22';
|
||||
parsed.devDependencies['@electron-forge/maker-rpm'] = '6.0.0-beta.22';
|
||||
parsed.devDependencies['@electron-forge/maker-squirrel'] = '6.0.0-beta.22';
|
||||
parsed.devDependencies['@electron-forge/maker-zip'] = '6.0.0-beta.22';
|
||||
parsed.devDependencies['@electron-forge/cli'] = '6.0.0-beta.34';
|
||||
parsed.devDependencies['@electron-forge/maker-deb'] = '6.0.0-beta.34';
|
||||
parsed.devDependencies['@electron-forge/maker-rpm'] = '6.0.0-beta.34';
|
||||
parsed.devDependencies['@electron-forge/maker-squirrel'] = '6.0.0-beta.34';
|
||||
parsed.devDependencies['@electron-forge/maker-zip'] = '6.0.0-beta.34';
|
||||
|
||||
// Scripts
|
||||
parsed.scripts = parsed.scripts || {};
|
||||
|
||||
+85
-47
@@ -1,4 +1,6 @@
|
||||
import { ElectronVersion, ElectronVersionSource, ElectronVersionState, NpmVersion } from '../interfaces';
|
||||
import semver from 'semver';
|
||||
import { RunnableVersion, Version, VersionSource, VersionState } from '../interfaces';
|
||||
import { normalizeVersion } from '../utils/normalize-version';
|
||||
|
||||
export const enum ElectronReleaseChannel {
|
||||
stable = 'Stable',
|
||||
@@ -7,17 +9,50 @@ export const enum ElectronReleaseChannel {
|
||||
unsupported = 'Unsupported'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sensible default version string.
|
||||
*
|
||||
* @param {Array<RunnableVersion>} knownVersions
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDefaultVersion(
|
||||
knownVersions: Array<RunnableVersion> = []
|
||||
): string {
|
||||
const ls = localStorage.getItem('version');
|
||||
|
||||
if (ls && knownVersions && knownVersions.find(({ version }) => version === ls)) {
|
||||
return ls;
|
||||
}
|
||||
|
||||
// Self-heal: Version not formated correctly
|
||||
const normalized = ls && normalizeVersion(ls);
|
||||
if (normalized) {
|
||||
if (knownVersions && knownVersions.find(({ version }) => version === normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
// Alright, the first version?
|
||||
const last = knownVersions && knownVersions[knownVersions.length - 1];
|
||||
if (last) {
|
||||
return last.version;
|
||||
}
|
||||
|
||||
// Report error
|
||||
throw new Error('Corrupted version data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the release channel for a given input
|
||||
* version.
|
||||
*
|
||||
* @param {NpmVersion} input
|
||||
* @param {Version | string} input
|
||||
* @returns {ElectronReleaseChannel}
|
||||
*/
|
||||
export function getReleaseChannel(
|
||||
input: NpmVersion
|
||||
input: Version | string
|
||||
): ElectronReleaseChannel {
|
||||
const tag = input.version || '';
|
||||
const tag = (typeof input === 'string') ? input : (input.version || '');
|
||||
|
||||
if (tag.includes('beta')) {
|
||||
return ElectronReleaseChannel.beta;
|
||||
@@ -44,17 +79,17 @@ export const enum VersionKeys {
|
||||
* Retrieve Electron versions from localStorage.
|
||||
*
|
||||
* @param {VersionKeys} key
|
||||
* @param {() => Array<NpmVersion>} fallbackMethod
|
||||
* @returns {Array<NpmVersion>}
|
||||
* @param {() => Array<Version>} fallbackMethod
|
||||
* @returns {Array<Version>}
|
||||
*/
|
||||
function getVersions(
|
||||
key: VersionKeys, fallbackMethod: () => Array<NpmVersion>
|
||||
): Array<NpmVersion> {
|
||||
key: VersionKeys, fallbackMethod: () => Array<Version>
|
||||
): Array<Version> {
|
||||
const fromLs = window.localStorage.getItem(key);
|
||||
|
||||
if (fromLs) {
|
||||
try {
|
||||
let result: Array<NpmVersion> = JSON.parse(fromLs);
|
||||
let result: Array<Version> = JSON.parse(fromLs);
|
||||
|
||||
if (!isExpectedFormat(result)) {
|
||||
// Known versions can just be downloaded again.
|
||||
@@ -80,9 +115,9 @@ function getVersions(
|
||||
* Save an array of GitHubVersions to localStorage.
|
||||
*
|
||||
* @param {VersionKeys} key
|
||||
* @param {Array<NpmVersion} versions
|
||||
* @param {Array<Version} versions
|
||||
*/
|
||||
function saveVersions(key: VersionKeys, versions: Array<NpmVersion>) {
|
||||
function saveVersions(key: VersionKeys, versions: Array<Version>) {
|
||||
const stringified = JSON.stringify(versions);
|
||||
window.localStorage.setItem(key, stringified);
|
||||
}
|
||||
@@ -90,35 +125,35 @@ function saveVersions(key: VersionKeys, versions: Array<NpmVersion>) {
|
||||
/**
|
||||
* Return both known as well as local versions.
|
||||
*
|
||||
* @returns {Array<NpmVersion>}
|
||||
* @returns {Array<Version>}
|
||||
*/
|
||||
export function getElectronVersions(): Array<ElectronVersion> {
|
||||
const known: Array<ElectronVersion> = getKnownVersions().map((version) => {
|
||||
export function getElectronVersions(): Array<RunnableVersion> {
|
||||
const known: Array<RunnableVersion> = getKnownVersions().map((version) => {
|
||||
return {
|
||||
...version,
|
||||
source: ElectronVersionSource.remote,
|
||||
state: ElectronVersionState.unknown
|
||||
source: VersionSource.remote,
|
||||
state: VersionState.unknown
|
||||
};
|
||||
});
|
||||
|
||||
const local: Array<ElectronVersion> = getLocalVersions().map((version) => {
|
||||
const local: Array<RunnableVersion> = getLocalVersions().map((version) => {
|
||||
return {
|
||||
...version,
|
||||
source: ElectronVersionSource.local,
|
||||
state: ElectronVersionState.ready
|
||||
source: VersionSource.local,
|
||||
state: VersionState.ready
|
||||
};
|
||||
});
|
||||
|
||||
return [ ...known, ...local ];
|
||||
return [...known, ...local];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a version to the local versions
|
||||
*
|
||||
* @param {NpmVersion} input
|
||||
* @returns {Array<NpmVersion>}
|
||||
* @param {Version} input
|
||||
* @returns {Array<Version>}
|
||||
*/
|
||||
export function addLocalVersion(input: NpmVersion): Array<NpmVersion> {
|
||||
export function addLocalVersion(input: Version): Array<Version> {
|
||||
const versions = getLocalVersions();
|
||||
|
||||
if (!versions.find((v) => v.localPath === input.localPath)) {
|
||||
@@ -133,9 +168,9 @@ export function addLocalVersion(input: NpmVersion): Array<NpmVersion> {
|
||||
/**
|
||||
* Retrieves local Electron versions, configured by the user.
|
||||
*
|
||||
* @returns {Array<NpmVersion>}
|
||||
* @returns {Array<Version>}
|
||||
*/
|
||||
export function getLocalVersions(): Array<NpmVersion> {
|
||||
export function getLocalVersions(): Array<Version> {
|
||||
const versions = getVersions(VersionKeys.local, () => []);
|
||||
|
||||
return versions;
|
||||
@@ -144,12 +179,12 @@ export function getLocalVersions(): Array<NpmVersion> {
|
||||
/**
|
||||
* Saves local versions to localStorage.
|
||||
*
|
||||
* @param {Array<NpmVersion>} versions
|
||||
* @param {Array<Version>} versions
|
||||
*/
|
||||
export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>) {
|
||||
export function saveLocalVersions(versions: Array<Version | RunnableVersion>) {
|
||||
const filteredVersions = versions.filter((v) => {
|
||||
if (isElectronVersion(v)) {
|
||||
return v.source === ElectronVersionSource.local;
|
||||
return v.source === VersionSource.local;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -162,18 +197,18 @@ export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>)
|
||||
* Retrieves our best guess regarding the latest Electron versions. Tries to
|
||||
* fetch them from localStorage, then from a static releases.json file.
|
||||
*
|
||||
* @returns {Array<NpmVersion>}
|
||||
* @returns {Array<Version>}
|
||||
*/
|
||||
export function getKnownVersions(): Array<NpmVersion> {
|
||||
export function getKnownVersions(): Array<Version> {
|
||||
return getVersions(VersionKeys.known, () => require('../../static/releases.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves known versions to localStorage.
|
||||
*
|
||||
* @param {Array<NpmVersion>} versions
|
||||
* @param {Array<Version>} versions
|
||||
*/
|
||||
export function saveKnownVersions(versions: Array<NpmVersion>) {
|
||||
export function saveKnownVersions(versions: Array<Version>) {
|
||||
return saveVersions(VersionKeys.known, versions);
|
||||
}
|
||||
|
||||
@@ -182,10 +217,10 @@ export function saveKnownVersions(versions: Array<NpmVersion>) {
|
||||
* saved after.
|
||||
*
|
||||
* @export
|
||||
* @returns {Promise<Array<ElectronVersion>>}
|
||||
* @returns {Promise<Array<RunnableVersion>>}
|
||||
*/
|
||||
export async function getUpdatedElectronVersions(
|
||||
): Promise<Array<ElectronVersion>> {
|
||||
): Promise<Array<RunnableVersion>> {
|
||||
try {
|
||||
await fetchVersions();
|
||||
} catch (error) {
|
||||
@@ -198,18 +233,21 @@ export async function getUpdatedElectronVersions(
|
||||
/**
|
||||
* Fetch the latest known versions directly from npm.
|
||||
*
|
||||
* @returns {Promise<Array<NpmVersion>>}
|
||||
* @returns {Promise<Array<Version>>}
|
||||
*/
|
||||
export async function fetchVersions() {
|
||||
const response = await window.fetch(`https://registry.npmjs.org/electron`);
|
||||
const response = await window.fetch('https://unpkg.com/electron-releases/lite.json');
|
||||
const data = await response.json();
|
||||
const versions: Record<string, any> = data.versions;
|
||||
|
||||
const output: Array<NpmVersion> = Object
|
||||
.keys(versions)
|
||||
.map((version) => ({ version }));
|
||||
// pre-0.24.0 versions were technically 'atom-shell' and cannot
|
||||
// be downloaded with @electron/get
|
||||
const MIN_DOWNLOAD_VERSION = '0.24.0';
|
||||
|
||||
if (output && output.length > 0 && isExpectedFormat(output)) {
|
||||
const output = data
|
||||
.map(({ version }: any) => ({ version }))
|
||||
.filter(({ version }: any) => semver.gte(version, MIN_DOWNLOAD_VERSION));
|
||||
|
||||
if (output?.length > 0 && isExpectedFormat(output)) {
|
||||
console.log(`Fetched new Electron versions (Count: ${output.length})`);
|
||||
saveKnownVersions(output);
|
||||
}
|
||||
@@ -231,9 +269,9 @@ export function isExpectedFormat(input: Array<any>): boolean {
|
||||
* Migrates old versions, if necessary
|
||||
*
|
||||
* @param {Array<any>} input
|
||||
* @returns {Array<NpmVersion>}
|
||||
* @returns {Array<Version>}
|
||||
*/
|
||||
export function migrateVersions(input: Array<any> = []): Array<NpmVersion> {
|
||||
export function migrateVersions(input: Array<any> = []): Array<Version> {
|
||||
return input
|
||||
.filter((item) => !!item)
|
||||
.map((item) => {
|
||||
@@ -247,11 +285,11 @@ export function migrateVersions(input: Array<any> = []): Array<NpmVersion> {
|
||||
localPath: url
|
||||
};
|
||||
})
|
||||
.filter((item) => !!item) as Array<NpmVersion>;
|
||||
.filter((item) => !!item) as Array<Version>;
|
||||
}
|
||||
|
||||
export function isElectronVersion(
|
||||
input: NpmVersion | ElectronVersion
|
||||
): input is ElectronVersion {
|
||||
return (input as ElectronVersion).source !== undefined;
|
||||
input: Version | RunnableVersion
|
||||
): input is RunnableVersion {
|
||||
return (input as RunnableVersion).source !== undefined;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user