Compare commits
1037
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
82fb8290c3 | ||
|
|
2eb8bb3cf5 | ||
|
|
3d6448030a | ||
|
|
65855908d1 | ||
|
|
9e986e24e2 | ||
|
|
9abc358bd9 | ||
|
|
7f7785c69c | ||
|
|
f36f300feb | ||
|
|
2c49214a25 | ||
|
|
c7a8163990 | ||
|
|
97995b0ec2 | ||
|
|
d2377dcc30 | ||
|
|
ac1197faaa | ||
|
|
9fb9aa6c92 | ||
|
|
6c5e41a2f1 | ||
|
|
4b52c81101 | ||
|
|
35f4f1293c | ||
|
|
8840b1ef7f | ||
|
|
d516522af2 | ||
|
|
0d0a51a900 | ||
|
|
be3b1e4a5b | ||
|
|
2acc7112b7 | ||
|
|
407bf3caeb | ||
|
|
49123116d5 | ||
|
|
9f6bc2f26f | ||
|
|
5585e2d5d0 | ||
|
|
4235969526 | ||
|
|
659d41f1c8 | ||
|
|
2549992782 | ||
|
|
b7314b83ee | ||
|
|
39c2a9164a | ||
|
|
1a9dc1fbf7 | ||
|
|
0eb01c4692 | ||
|
|
96667a3b04 | ||
|
|
4365513f57 | ||
|
|
913b37c285 | ||
|
|
66c55d39c0 | ||
|
|
0f3a3aca72 | ||
|
|
3fc974a758 | ||
|
|
72b805059c | ||
|
|
291882ac5c | ||
|
|
3c05cc8ec7 | ||
|
|
75d2f9ff72 | ||
|
|
0119f0ce69 | ||
|
|
8ed5ea94b5 | ||
|
|
deaac4e15c | ||
|
|
48e58c095d | ||
|
|
28f0eca429 | ||
|
|
529906e1a9 | ||
|
|
40c9aaeb7a | ||
|
|
15924c7741 | ||
|
|
c62041a000 | ||
|
|
ff3e931d14 | ||
|
|
7cc16b6fab | ||
|
|
45e8e311da | ||
|
|
b09e792583 | ||
|
|
cdb862442c | ||
|
|
1e1566dbdb | ||
|
|
e571fcca4f | ||
|
|
e61e49987e | ||
|
|
133411c150 | ||
|
|
db8c7c32ab | ||
|
|
003e27f57e | ||
|
|
72d717bd48 | ||
|
|
675500fe25 | ||
|
|
fbe2311d53 | ||
|
|
ef42b1affe | ||
|
|
2adde7d9ce | ||
|
|
5fa0ddcc73 | ||
|
|
5d501f0827 | ||
|
|
d738936952 | ||
|
|
57ce8b0b79 | ||
|
|
46f4efb4b4 | ||
|
|
10a696a9d7 | ||
|
|
585c0e1183 | ||
|
|
946d32fdd0 | ||
|
|
f09363ec26 | ||
|
|
1a6876497a | ||
|
|
6bc04f5d2a | ||
|
|
9f7bf9d299 | ||
|
|
3b7873917f | ||
|
|
4616f12d6c | ||
|
|
63f37c9553 | ||
|
|
09edfa16b0 | ||
|
|
da3a607bc1 | ||
|
|
5fcc34b035 | ||
|
|
dfed297226 | ||
|
|
7cb2c4d188 | ||
|
|
f0215d8c90 | ||
|
|
ffdb57e2f3 | ||
|
|
e5dd6ffb4e | ||
|
|
1083b6c2de | ||
|
|
73b6cb0cb2 | ||
|
|
3e5914b192 | ||
|
|
fe81d5c14c | ||
|
|
c85536a8b9 | ||
|
|
8d90dbed09 | ||
|
|
b918c13c72 | ||
|
|
570bab9c29 | ||
|
|
f0f7ed22ee | ||
|
|
a62c1550a1 | ||
|
|
754473e10a | ||
|
|
732f81cd33 | ||
|
|
47e5182117 | ||
|
|
92c831cc7b | ||
|
|
8ffb215797 | ||
|
|
616264f017 | ||
|
|
ee6e474b04 | ||
|
|
ee8fa678f9 | ||
|
|
e605ef399d | ||
|
|
e653cb5c0f | ||
|
|
44df77b7e8 | ||
|
|
99db07e000 | ||
|
|
0f7498e5ea | ||
|
|
b30a3cb493 | ||
|
|
6686f8f1b3 | ||
|
|
5db586b34d | ||
|
|
076c3dedaa | ||
|
|
2bf245bbb9 | ||
|
|
6445d82dfa | ||
|
|
bbbbea8a27 | ||
|
|
7f4e6c70d9 | ||
|
|
ebbd98b1dd | ||
|
|
712707026c | ||
|
|
595ad0e5ab | ||
|
|
4c09f69435 | ||
|
|
deb6bd6323 | ||
|
|
db7b8861b5 | ||
|
|
ca5600d521 | ||
|
|
619d48bfe3 | ||
|
|
ab7dff11c9 | ||
|
|
877a2ec434 | ||
|
|
7806bb47c8 | ||
|
|
25f73e5390 | ||
|
|
9eeecc94d5 | ||
|
|
bcd37ff67c | ||
|
|
6099c7b1ea | ||
|
|
237ce59100 | ||
|
|
0289a92a06 | ||
|
|
86fb981c98 | ||
|
|
3d7cd9eeba | ||
|
|
066da6433e | ||
|
|
b83378df5a | ||
|
|
5aaf46beb9 | ||
|
|
066195ed39 | ||
|
|
96ea7bfe61 | ||
|
|
0e68605a05 | ||
|
|
7a5d84908f | ||
|
|
e3ff84f14e | ||
|
|
37d6f63b33 | ||
|
|
8867679f3a | ||
|
|
12d9892be5 | ||
|
|
28ccff3254 | ||
|
|
e8ab965579 | ||
|
|
e0d5ceacdf | ||
|
|
d62a2a2c78 | ||
|
|
e5d2bfa7a3 | ||
|
|
0e05a34efa | ||
|
|
1f4f489947 | ||
|
|
9cab776159 | ||
|
|
26e3f09b95 | ||
|
|
3c9f06909b | ||
|
|
779f273339 | ||
|
|
1cdfb0f031 | ||
|
|
6103db3a80 | ||
|
|
8b63134324 | ||
|
|
60e58dbd5a | ||
|
|
38b7b08691 | ||
|
|
5acdcc4ab2 | ||
|
|
8dd587ac7e | ||
|
|
aba46cd666 | ||
|
|
3286852006 | ||
|
|
2682a3f67c | ||
|
|
6e7fb45334 | ||
|
|
0ef82b6ad6 | ||
|
|
41c6dd3d44 | ||
|
|
c6f90331b9 | ||
|
|
d7b00c4166 | ||
|
|
3fef83356d | ||
|
|
34be3369f3 | ||
|
|
5c04d74ebc | ||
|
|
29ac8334fe | ||
|
|
604251260d | ||
|
|
7015e49725 | ||
|
|
d3a19c9b3c | ||
|
|
6958192096 | ||
|
|
100be9548e | ||
|
|
bff1e23be3 | ||
|
|
8e6ddd59fb | ||
|
|
419b5f57a0 | ||
|
|
27858cac6a | ||
|
|
ac7221b117 | ||
|
|
31c898f082 | ||
|
|
1bc381571c | ||
|
|
33902dbe01 | ||
|
|
2285b14184 | ||
|
|
b67654769f | ||
|
|
8cc6f766a5 | ||
|
|
c21e398c89 | ||
|
|
8019951e8c | ||
|
|
707ee2192f | ||
|
|
2a4e59461e | ||
|
|
22440298d7 | ||
|
|
37168a99ee | ||
|
|
40254126d0 | ||
|
|
492893b71a | ||
|
|
a1c91727ae | ||
|
|
634d0ca734 | ||
|
|
1557841c81 | ||
|
|
ff3fbac7ed | ||
|
|
82f83acba4 | ||
|
|
f957e0c53a | ||
|
|
bd1915a0f8 | ||
|
|
01413c8830 | ||
|
|
62002debd4 | ||
|
|
7fc1008121 | ||
|
|
aaf183614f | ||
|
|
4a2a73b780 | ||
|
|
a5985e8dde | ||
|
|
b5bf652df0 | ||
|
|
79cdad1291 | ||
|
|
35b7b8994f | ||
|
|
1ca25fe515 | ||
|
|
82976cb269 | ||
|
|
6487db8207 | ||
|
|
325cb3416a | ||
|
|
73ea0873f8 | ||
|
|
987d05dd0f | ||
|
|
61e8524996 | ||
|
|
82b0fea020 | ||
|
|
caff29dc39 | ||
|
|
de1a908a6e | ||
|
|
766cf9100a | ||
|
|
27dc99e7cb | ||
|
|
d711082c67 | ||
|
|
7ac567e522 | ||
|
|
b074071333 | ||
|
|
82716bf02c | ||
|
|
11d062e6e0 | ||
|
|
05ca117f02 | ||
|
|
05e2785aa4 | ||
|
|
c2bc958504 | ||
|
|
f2829e851c | ||
|
|
b3d8971a1a | ||
|
|
b3c39f1546 | ||
|
|
a3ab1405a3 | ||
|
|
56632c94a0 | ||
|
|
431b3d5bbc | ||
|
|
b4e1740294 | ||
|
|
0d39c35081 | ||
|
|
685637e6c9 | ||
|
|
1b7f913bcf | ||
|
|
5b0d17c88a | ||
|
|
accf71da44 | ||
|
|
b2ff94744a | ||
|
|
58c656c2f3 | ||
|
|
059dff3561 | ||
|
|
32c4b6c804 | ||
|
|
8b8197d3c8 | ||
|
|
dfd5afd115 | ||
|
|
0336a4292e | ||
|
|
c39b84da3e | ||
|
|
4c779b6033 | ||
|
|
2ebb46a988 | ||
|
|
3413e1cb9f | ||
|
|
b1808ef8e6 | ||
|
|
558431a85c | ||
|
|
3edbc019c6 | ||
|
|
06af8ba35f | ||
|
|
0aed40af5c | ||
|
|
bec4e2fd86 | ||
|
|
5cd5881d35 | ||
|
|
fc3a7b2960 | ||
|
|
3f102b8ee7 | ||
|
|
46450ee768 | ||
|
|
33f9f9c1c3 | ||
|
|
b731ae710c | ||
|
|
c486b0ee78 | ||
|
|
9e7dd4dc06 | ||
|
|
99a37fbd82 | ||
|
|
e27f7f29c1 | ||
|
|
0b50e952ac | ||
|
|
58b252f7f2 | ||
|
|
5dffe5fce8 | ||
|
|
2b6e4ee907 | ||
|
|
9d85ae36d5 | ||
|
|
0f1ff6a70b | ||
|
|
1564284e30 | ||
|
|
dd46a0fb7f | ||
|
|
2536c0fc89 | ||
|
|
ff700845fb | ||
|
|
e74a2876b3 | ||
|
|
440633ae3c | ||
|
|
6b0ae32263 | ||
|
|
7d49f9076c | ||
|
|
7b2dbc5e1b | ||
|
|
331446a031 | ||
|
|
62abe7e6e0 | ||
|
|
c6ba5399a6 | ||
|
|
b63302bb4b | ||
|
|
9409b3280f | ||
|
|
5b3237d437 | ||
|
|
b606fa2bfe | ||
|
|
746ed2fd5d | ||
|
|
724fd39ba0 | ||
|
|
65875235fe | ||
|
|
2d09369d6c | ||
|
|
ee48344b9a | ||
|
|
bed5d7f739 | ||
|
|
efc2d6b3c4 | ||
|
|
aab01abff4 | ||
|
|
23aca61e4a | ||
|
|
a057b2a26a | ||
|
|
8f754fb356 | ||
|
|
528aba15e7 | ||
|
|
54dd7f8d3a | ||
|
|
6441c266b5 | ||
|
|
66f19c3c6f | ||
|
|
ce7ba3cac1 | ||
|
|
9e53b09941 | ||
|
|
6522890bcf | ||
|
|
8d9776da6a | ||
|
|
78905d9b6a | ||
|
|
cfa0620aab | ||
|
|
9e7cb2808e | ||
|
|
eb12f86725 | ||
|
|
d3651ef4ca | ||
|
|
6acce843c3 | ||
|
|
be6c1b0781 | ||
|
|
241bec88b0 | ||
|
|
48e5939d10 | ||
|
|
2389e07781 | ||
|
|
56a4c8f641 | ||
|
|
c4a7929d82 | ||
|
|
856b0cc437 | ||
|
|
93613a421a | ||
|
|
ea25ee4d78 | ||
|
|
20d3f351ff | ||
|
|
c64bb2c8ea | ||
|
|
5153921e43 | ||
|
|
aa763b839d | ||
|
|
fd1984d140 | ||
|
|
b63fe6bda2 | ||
|
|
a296945e0d | ||
|
|
4938c0cd01 | ||
|
|
cf30c9493e | ||
|
|
7a544cdd8e | ||
|
|
7e43acdd53 | ||
|
|
7ffe79c9e1 | ||
|
|
d68588aeb2 | ||
|
|
4b60316ebc | ||
|
|
4b5908b97d | ||
|
|
15f397f829 | ||
|
|
e51f653ed9 | ||
|
|
8168ec9d26 | ||
|
|
b11e6bd29e | ||
|
|
3ca0426994 | ||
|
|
4106a63ee3 | ||
|
|
ac4b5797fe | ||
|
|
d66fecfb90 | ||
|
|
50bbb895a9 | ||
|
|
970f06274a | ||
|
|
8ea6446dca | ||
|
|
37e61a9cda | ||
|
|
6defceb8d5 | ||
|
|
4e7349c48b | ||
|
|
296396473c | ||
|
|
a54a9d631e | ||
|
|
2e7c9ba7e4 | ||
|
|
f58058a7da | ||
|
|
d3f3c047e9 | ||
|
|
4c1ec07684 | ||
|
|
d4ccc0d17f | ||
|
|
edacde350f | ||
|
|
48b49f48d0 | ||
|
|
f21a4c72f6 | ||
|
|
e2bb88d4c5 | ||
|
|
98d848a443 | ||
|
|
e9a368adde | ||
|
|
1194554a32 | ||
|
|
816337f39c | ||
|
|
20a12db00d | ||
|
|
8037a2e418 | ||
|
|
aed4c40956 | ||
|
|
d20d29c481 | ||
|
|
fcaa6b7a59 | ||
|
|
08dd132a4f | ||
|
|
92417e488c | ||
|
|
abc60ea21f | ||
|
|
4a3a6d50f5 | ||
|
|
a9d70f730c | ||
|
|
2d184f1d72 | ||
|
|
abc251c318 | ||
|
|
001f377e29 | ||
|
|
fbad873941 | ||
|
|
ac5d34fe92 | ||
|
|
7b5ee87c00 | ||
|
|
1baf6459f1 | ||
|
|
9be0559595 | ||
|
|
3dae0bfb44 | ||
|
|
aabf2827c1 | ||
|
|
2f77d1f85f | ||
|
|
14e4f0cf32 | ||
|
|
ee809369e1 | ||
|
|
0026453984 | ||
|
|
8f727af18f | ||
|
|
de535f3ac5 | ||
|
|
5a89324bbd | ||
|
|
c9fd2bfb97 | ||
|
|
c5f61a8351 | ||
|
|
2d55803ba0 | ||
|
|
06f81f0d24 | ||
|
|
28c1bf65b6 | ||
|
|
23f1812e14 | ||
|
|
f3a1e153cd | ||
|
|
9ca0df0f3e | ||
|
|
1bfa6033d3 | ||
|
|
03945118ce | ||
|
|
48f9beba4d | ||
|
|
02bf3e3837 | ||
|
|
7bc6fa1ee0 | ||
|
|
fa8898ed10 | ||
|
|
d226f29219 | ||
|
|
df89ff581a | ||
|
|
1cf4759483 | ||
|
|
9c061b63e2 | ||
|
|
2b92a391fa | ||
|
|
cd99cda65c | ||
|
|
57c0dee597 | ||
|
|
ea3a3473e8 | ||
|
|
9c29590c90 | ||
|
|
64c12b7245 | ||
|
|
b996446d1c | ||
|
|
3a928e2f52 | ||
|
|
b328798c93 | ||
|
|
3087b57dff | ||
|
|
06e46959f8 | ||
|
|
67ed669a36 | ||
|
|
680c4b140f | ||
|
|
3830828e6f | ||
|
|
a509763b1e | ||
|
|
4f62229013 | ||
|
|
42cff3e318 | ||
|
|
98e7e31b1a | ||
|
|
bede240aee | ||
|
|
ffe9c8fa52 | ||
|
|
3e3e9be30e | ||
|
|
fa26ec6ffb | ||
|
|
068c4ad14f | ||
|
|
abac62c3d8 | ||
|
|
d137a3761f | ||
|
|
6980d50192 | ||
|
|
0c421f0b38 | ||
|
|
84043e453f | ||
|
|
45e7cb533e | ||
|
|
8fe3875d9a | ||
|
|
3e27cbae84 | ||
|
|
a6e6fad4d9 | ||
|
|
09928cdeb3 | ||
|
|
49a1d057f2 | ||
|
|
3ee349d8f1 | ||
|
|
00cb6ecf73 | ||
|
|
b6b5c0ac57 | ||
|
|
08751d2c61 | ||
|
|
0725048f64 | ||
|
|
dc18199790 | ||
|
|
ef85111944 | ||
|
|
2cef76865c | ||
|
|
befa3192f4 | ||
|
|
c21b157469 | ||
|
|
51e7c609da | ||
|
|
cc493283bd | ||
|
|
41203ecd17 | ||
|
|
d9d5335e93 | ||
|
|
e968219491 | ||
|
|
5e973723c6 | ||
|
|
1f6c2e9e89 | ||
|
|
bd1f4e9a0d | ||
|
|
e829e3a17e | ||
|
|
11bf84189f | ||
|
|
12f979b25e | ||
|
|
70ae2e1806 | ||
|
|
546eee8d63 | ||
|
|
b5ea4a869b | ||
|
|
e95976006e | ||
|
|
97a37ab800 | ||
|
|
67aa09e717 | ||
|
|
d371e4ee9f | ||
|
|
4ce070c24b | ||
|
|
c1320453a0 | ||
|
|
b20c31b2da | ||
|
|
a789b3fa60 | ||
|
|
a814118a19 | ||
|
|
cec54617be | ||
|
|
ac7bf44a25 | ||
|
|
5c98534cd8 | ||
|
|
e085e90a7a | ||
|
|
ab62d3bc10 | ||
|
|
5fe535ca29 | ||
|
|
f3904b0226 | ||
|
|
01ae77d64c | ||
|
|
a74b7ae6d8 | ||
|
|
ccdd8d7fc6 | ||
|
|
a8a7764d64 | ||
|
|
94ea9fc432 | ||
|
|
ed66edc612 | ||
|
|
a5c6695c4e | ||
|
|
943d8f314f | ||
|
|
04c65f0d60 | ||
|
|
7d8983eaea | ||
|
|
fd812f9d0b | ||
|
|
e45a25970e | ||
|
|
f3e9faa64e | ||
|
|
516c7e6816 | ||
|
|
c80bfcc54f | ||
|
|
48fd3960b1 | ||
|
|
2691486e73 | ||
|
|
e66116cdb4 | ||
|
|
3d1450f35a | ||
|
|
1d010be01f | ||
|
|
1214963212 | ||
|
|
e4f8c3d434 | ||
|
|
5ea418f18e | ||
|
|
aad2b26de8 | ||
|
|
00d1b3c56d | ||
|
|
74ea847327 | ||
|
|
e53b40ca08 | ||
|
|
bc79c66526 | ||
|
|
1a1f01f639 | ||
|
|
a89d5ef91c | ||
|
|
6aee1b1d68 | ||
|
|
055d6cc548 | ||
|
|
9b86273aad | ||
|
|
5cdebdd86a | ||
|
|
689ef5d926 | ||
|
|
8241c29521 | ||
|
|
2bebba5ff6 | ||
|
|
7d8b7c6ed3 | ||
|
|
38dc15c1ab | ||
|
|
9d65cc1083 | ||
|
|
260ada1ced | ||
|
|
a04030904f | ||
|
|
d5143a9224 | ||
|
|
60df3aecc4 | ||
|
|
7016affadf | ||
|
|
d32f9af011 | ||
|
|
bbbe7266af | ||
|
|
67b2819a85 | ||
|
|
851ca5b390 | ||
|
|
a28b9d34da | ||
|
|
faf9c96fe2 | ||
|
|
344382a18d | ||
|
|
1865f66e29 | ||
|
|
fd458adc5e | ||
|
|
57b00c8126 | ||
|
|
dcbc9eff88 | ||
|
|
b52ee70dde | ||
|
|
316077e3b7 | ||
|
|
80993f2cac | ||
|
|
59ac41b369 | ||
|
|
6dffdef8f0 |
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
+40
-3
@@ -1,3 +1,40 @@
|
||||
node_modules
|
||||
out
|
||||
dist
|
||||
.DS_Store
|
||||
.env
|
||||
.gclient_done
|
||||
**/.npmrc
|
||||
.tags*
|
||||
.vs/
|
||||
.vscode/
|
||||
*.log
|
||||
*.pyc
|
||||
*.sln
|
||||
*.swp
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
*.vcxproj
|
||||
*.vcxproj.filters
|
||||
*.vcxproj.user
|
||||
*.xcodeproj
|
||||
/.idea/
|
||||
/dist/
|
||||
/external_binaries/
|
||||
/out/
|
||||
node_modules/
|
||||
SHASUMS256.txt
|
||||
**/yarn.lock
|
||||
compile_commands.json
|
||||
.envrc
|
||||
.cache
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
|
||||
# Coverage
|
||||
coverage
|
||||
|
||||
# npm package
|
||||
/npm/dist
|
||||
/npm/path.txt
|
||||
|
||||
# Electron Fiddle-Specific
|
||||
/static/contributors.json
|
||||
/static/css/root.css
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"ignoreFiles": [
|
||||
"./src/less/*-vendor.less"
|
||||
],
|
||||
"rules": { }
|
||||
}
|
||||
+35
-4
@@ -1,10 +1,10 @@
|
||||
language: node_js
|
||||
node_js: "8"
|
||||
node_js: "12"
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
dist: trusty
|
||||
osx_image: xcode8.3
|
||||
osx_image: xcode10
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
@@ -25,9 +25,40 @@ branches:
|
||||
|
||||
install:
|
||||
- npm install
|
||||
- npm update
|
||||
- |
|
||||
if [[ "$TRAVIS_OS_NAME" == "osx" && "$TRAVIS_SECURE_ENV_VARS" == "true" ]]; 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
|
||||
|
||||
script:
|
||||
- npm run lint
|
||||
- npm run test:ci
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then npm run test:coverage; fi
|
||||
- if test -z "$TRAVIS_TAG"; then npm run make; fi
|
||||
after_success: if test -n "$TRAVIS_TAG"; then npm run publish; fi
|
||||
|
||||
after_success: if test -n "$TRAVIS_TAG"; then npm run publish; fi
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct:
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [coc@electronjs.org](mailto:coc@electronjs.org). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor-Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: https://contributor-covenant.org
|
||||
[version]: https://contributor-covenant.org/version/1/4/
|
||||
@@ -1,5 +1,68 @@
|
||||
# Electron Fiddle
|
||||
# <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
|
||||
|
||||
> Still a work in progress, please stay tuned.
|
||||
[](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 published
|
||||
on GitHub, anyone can quickly try your Fiddle out by just entering it in the
|
||||
address bar.
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://github.com/electron/fiddle/releases/latest">
|
||||
Download Electron Fiddle for macOS, Windows, and Linux
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<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
|
||||
available in Electron, so if you want to quickly see what a
|
||||
[BrowserView][BrowserView] is or how the [desktopCapturer][desktopCapturer]
|
||||
works, Fiddle has got you covered.
|
||||
|
||||
### Code with Types
|
||||
|
||||

|
||||
|
||||
Fiddle includes Microsoft's excellent Monaco Editor, the same editor powering
|
||||
Visual Studio Code. It also installs the type definitions for the currently
|
||||
selected version of Electron automatically, ensuring that you always have
|
||||
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
|
||||
[electron-forge][electron-forge], allowing you to package your fiddle as an
|
||||
app for Windows, macOS, or Linux.
|
||||
|
||||
### Start with Fiddle, Continue Wherever
|
||||
|
||||

|
||||
|
||||
Fiddle is not an IDE – it is however an excellent starting point. Once your
|
||||
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!
|
||||
|
||||
## License
|
||||
|
||||
[MIT, please see the LICENSE file for full details](https://github.com/electron/fiddle/blob/master/LICENSE.md).
|
||||
|
||||
When using the Electron or other GitHub logos, be sure to follow the [GitHub
|
||||
logo guidelines](https://github.com/logos).
|
||||
|
||||
[BrowserView]: https://electronjs.org/docs/api/browser-view
|
||||
[desktopCapturer]: https://electronjs.org/docs/api/desktop-capturer
|
||||
[electron-forge]: https://electronforge.io/
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# Reporting Security Issues
|
||||
|
||||
The Electron team and community take security bugs in Electron seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
||||
|
||||
To report a security issue, email [security@electronjs.org](mailto:security@electronjs.org) and include the word "SECURITY" in the subject line.
|
||||
|
||||
The Electron team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
||||
|
||||
Report security bugs in third-party modules to the person or team maintaining the module. You can also report a vulnerability through the [Node Security Project](https://nodesecurity.io/report).
|
||||
|
||||
## Learning More About Security
|
||||
To learn more about securing an Electron application, please see the [security tutorial](https://github.com/electron/electron/blob/master/docs/tutorial/security.md).
|
||||
@@ -0,0 +1,37 @@
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "12"
|
||||
|
||||
init:
|
||||
- git config --global core.symlinks true
|
||||
|
||||
install:
|
||||
# Setup the code signing certificate
|
||||
- ps: >-
|
||||
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)
|
||||
}
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- node --version
|
||||
- npm ci
|
||||
|
||||
cache:
|
||||
- '%APPDATA%\npm-cache -> appveyor.yml'
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm run lint
|
||||
- npm run test
|
||||
|
||||
artifacts:
|
||||
- path: 'out\make\squirrel.windows\**\*.exe'
|
||||
|
||||
build_script:
|
||||
- if %APPVEYOR_REPO_TAG% EQU false npm run make
|
||||
- if %APPVEYOR_REPO_TAG% EQU true npm run publish
|
||||
- if %APPVEYOR_REPO_TAG% EQU true npm run publish -- --arch=ia32
|
||||
- ps: Tree ./out/make /F
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="SVG" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Icon">
|
||||
<circle id="BG" stroke="#FFFFFF" stroke-width="36" fill="#E79537" cx="512" cy="512" r="430"></circle>
|
||||
<g id="Glyph" transform="translate(336.000000, 280.000000)">
|
||||
<path d="M92,335.02442 L92,44 C92,30.745166 102.745166,20 116,20 L356,20 C369.254834,20 380,30.745166 380,44 C380,57.254834 369.254834,68 356,68 L140,68 L140,188 L239.428571,188 C250.789858,188 260,198.745166 260,212 C260,225.254834 250.789858,236 239.428571,236 L140,236 L140,335.02442 C181.405453,345.681447 212,383.267854 212,428 C212,481.019336 169.019336,524 116,524 C62.980664,524 20,481.019336 20,428 C20,383.267854 50.5945469,345.681447 92,335.02442 Z" id="Shadow" fill="#000000" fill-rule="nonzero" opacity="0.0980980283"></path>
|
||||
<path d="M72,315.02442 L72,24 C72,10.745166 82.745166,0 96,0 L336,0 C349.254834,0 360,10.745166 360,24 C360,37.254834 349.254834,48 336,48 L120,48 L120,168 L219.428571,168 C230.789858,168 240,178.745166 240,192 C240,205.254834 230.789858,216 219.428571,216 L120,216 L120,315.02442 C161.405453,325.681447 192,363.267854 192,408 C192,461.019336 149.019336,504 96,504 C42.980664,504 0,461.019336 0,408 C0,363.267854 30.5945469,325.681447 72,315.02442 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
<path d="M96,456 C122.509668,456 144,434.509668 144,408 C144,381.490332 122.509668,360 96,360 C69.490332,360 48,381.490332 48,408 C48,434.509668 69.490332,456 96,456 Z" id="Dot" fill="#EB5E00"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
+106
-14
@@ -1,26 +1,89 @@
|
||||
/* tslint:disable */
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {},
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const { version } = packageJson
|
||||
const iconDir = path.resolve(__dirname, 'assets', 'icons')
|
||||
|
||||
const config = {
|
||||
hooks: {
|
||||
generateAssets: require('./tools/generateAssets')
|
||||
},
|
||||
packagerConfig: {
|
||||
name: 'Electron Fiddle',
|
||||
executableName: 'electron-fiddle',
|
||||
asar: true,
|
||||
icon: path.resolve(__dirname, 'assets', 'icons', 'fiddle'),
|
||||
// TODO: FIXME?
|
||||
// ignore: [
|
||||
// /^\/\.vscode\//,
|
||||
// /^\/tools\//
|
||||
// ],
|
||||
appBundleId: 'com.electron.fiddle',
|
||||
appCategoryType: 'public.app-category.developer-tools',
|
||||
protocols: [{
|
||||
name: 'Electron Fiddle Launch Protocol',
|
||||
schemes: ['electron-fiddle']
|
||||
}],
|
||||
win32metadata: {
|
||||
CompanyName: 'Electron Community',
|
||||
OriginalFilename: 'Electron Fiddle',
|
||||
},
|
||||
osxSign: {
|
||||
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: {
|
||||
name: "electron_fiddle"
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
platforms: ['win32'],
|
||||
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',
|
||||
exe: 'electron-fiddle.exe',
|
||||
iconUrl: 'https://raw.githubusercontent.com/electron/fiddle/0119f0ce697f5ff7dec4fe51f17620c78cfd488b/assets/icons/fiddle.ico',
|
||||
loadingGif: './assets/loading.gif',
|
||||
noMsi: true,
|
||||
remoteReleases: '',
|
||||
setupExe: `electron-fiddle-${version}-${arch}-setup.exe`,
|
||||
setupIcon: path.resolve(iconDir, 'fiddle.ico'),
|
||||
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD,
|
||||
certificateFile
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "@electron-forge/maker-zip",
|
||||
platforms: ["darwin"]
|
||||
name: '@electron-forge/maker-zip',
|
||||
platforms: ['darwin']
|
||||
},
|
||||
{
|
||||
name: "@electron-forge/maker-deb",
|
||||
platforms: ["linux"]
|
||||
name: '@electron-forge/maker-deb',
|
||||
platforms: ['linux'],
|
||||
config: {
|
||||
icon: {
|
||||
scalable: path.resolve(iconDir, 'fiddle.svg')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "@electron-forge/maker-rpm",
|
||||
platforms: ["linux"]
|
||||
name: '@electron-forge/maker-rpm',
|
||||
platforms: ['linux']
|
||||
}
|
||||
],
|
||||
publishers: [
|
||||
@@ -31,8 +94,37 @@ module.exports = {
|
||||
owner: 'electron',
|
||||
name: 'fiddle'
|
||||
},
|
||||
prerelease: true
|
||||
draft: true,
|
||||
prerelease: false
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function notarizeMaybe() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.CI) {
|
||||
console.log(`Not in CI, skipping notarization`);
|
||||
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
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"transform": {
|
||||
".(js|jsx|ts|tsx)": "ts-jest"
|
||||
},
|
||||
"testURL": "http://localhost",
|
||||
"testRegex": "(-spec)\\.(ts|tsx)$",
|
||||
"bail": true,
|
||||
"resetMocks": true,
|
||||
"resetModules": true,
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/tests/setup.js"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"jsx",
|
||||
"json",
|
||||
"ts",
|
||||
"tsx"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"/.tmp/"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"json",
|
||||
"html",
|
||||
"lcov"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"!**/*.d.ts",
|
||||
"!**/*constants.ts"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"/tests/.*\\.(ts|js)$"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
".*releases.json$": "<rootDir>/static/releases.json"
|
||||
}
|
||||
}
|
||||
Generated
+15600
-2398
File diff suppressed because it is too large
Load Diff
+90
-47
@@ -1,65 +1,108 @@
|
||||
{
|
||||
"name": "electron-fiddle",
|
||||
"productName": "electron-fiddle",
|
||||
"version": "1.0.0",
|
||||
"description": "My Electron application description",
|
||||
"productName": "Electron Fiddle",
|
||||
"version": "0.11.1",
|
||||
"description": "The easiest way to get started with Electron",
|
||||
"repository": "https://github.com/electron/fiddle",
|
||||
"main": "./dist/main",
|
||||
"main": "./dist/src/main/main",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "npm run build && electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"contributors": "node ./tools/contributors.js",
|
||||
"less": "node ./tools/lessc.js",
|
||||
"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/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",
|
||||
"lint": "tslint -c tslint.json -p tsconfig.json -e \"node_modules/**/*.ts\""
|
||||
"start": "rimraf ./dist && electron-forge start",
|
||||
"test": "jest --config=jest.json --coverage",
|
||||
"test:ci": "jest --config=jest.json --coverage --runInBand",
|
||||
"test:coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
|
||||
"tsc": "node ./tools/tsc.js",
|
||||
"electron-releases": "node ./tools/fetch-releases.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "felixr",
|
||||
"keywords": [
|
||||
"Electron",
|
||||
"Fiddle",
|
||||
"Tutorial"
|
||||
],
|
||||
"author": "Felix Rieseberg <felix@felixrieseberg.com>",
|
||||
"license": "MIT",
|
||||
"config": {
|
||||
"forge": "./forge.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.13",
|
||||
"@fortawesome/react-fontawesome": "0.0.20",
|
||||
"builtin-modules": "^3.0.0",
|
||||
"electron-download": "^4.1.0",
|
||||
"@blueprintjs/core": "^3.19.1",
|
||||
"@blueprintjs/select": "^3.11.1",
|
||||
"@octokit/rest": "^16.34.0",
|
||||
"builtin-modules": "^3.1.0",
|
||||
"classnames": "^2.2.6",
|
||||
"electron-default-menu": "^1.0.1",
|
||||
"electron-devtools-installer": "^2.2.4",
|
||||
"electron-download": "^4.1.1",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"extract-zip": "^1.6.7",
|
||||
"fs-extra": "^6.0.1",
|
||||
"loader-utils": "^1.1.0",
|
||||
"milligram": "^1.3.0",
|
||||
"mobx": "^4.3.1",
|
||||
"mobx-react": "^5.2.0",
|
||||
"monaco-loader": "^0.8.2",
|
||||
"react": "^16.4.0",
|
||||
"react-dom": "^16.4.0",
|
||||
"semver": "^5.5.0",
|
||||
"tmp": "0.0.33",
|
||||
"tslib": "^1.9.2",
|
||||
"update-electron-app": "^1.2.0"
|
||||
"fix-path": "^2.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"mobx": "^5.14.2",
|
||||
"mobx-react": "^6.1.4",
|
||||
"monaco-editor": "^0.18.1",
|
||||
"monaco-loader": "1.0.0",
|
||||
"namor": "^1.1.3",
|
||||
"react": "^16.11.0",
|
||||
"react-dom": "^16.11.0",
|
||||
"react-mosaic-component": "^3.2.0",
|
||||
"semver": "^6.3.0",
|
||||
"tmp": "0.1.0",
|
||||
"tslib": "^1.10.0",
|
||||
"update-electron-app": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.0.0-beta.17",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.17",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.17",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.17",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.17",
|
||||
"@types/builtin-modules": "^2.0.0",
|
||||
"@types/fs-extra": "^5.0.2",
|
||||
"@types/node": "^10.3.1",
|
||||
"@types/react": "^16.3.16",
|
||||
"@types/react-dom": "^16.0.6",
|
||||
"@types/semver": "^5.5.0",
|
||||
"@types/tmp": "0.0.33",
|
||||
"css-loader": "^0.28.11",
|
||||
"electron": "2.0.2",
|
||||
"monaco-editor": "^0.13.1",
|
||||
"style-loader": "^0.21.0",
|
||||
"tslint": "^5.10.0",
|
||||
"tslint-microsoft-contrib": "^5.0.3",
|
||||
"tslint-react": "^3.6.0",
|
||||
"typescript": "^2.9.1"
|
||||
"@babel/core": "^7.6.4",
|
||||
"@electron-forge/cli": "^6.0.0-beta.46",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.46",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.46",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.46",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.46",
|
||||
"@electron-forge/publisher-github": "^6.0.0-beta.46",
|
||||
"@types/builtin-modules": "^3.1.1",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/fs-extra": "^8.0.1",
|
||||
"@types/jest": "^24.0.19",
|
||||
"@types/log-symbols": "^3.0.0",
|
||||
"@types/node": "^12.11.7",
|
||||
"@types/react": "^16.9.11",
|
||||
"@types/react-dom": "^16.9.3",
|
||||
"@types/semver": "^6.0.2",
|
||||
"@types/tmp": "0.1.0",
|
||||
"chokidar": "^3.2.2",
|
||||
"coveralls": "^3.0.7",
|
||||
"electron": "7.1.3",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.15.1",
|
||||
"enzyme-to-json": "^3.4.3",
|
||||
"jest": "^24.9.0",
|
||||
"jest-fetch-mock": "^2.1.2",
|
||||
"less": "^3.10.3",
|
||||
"log-symbols": "^3.0.0",
|
||||
"node-abi": "^2.12.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"react-test-renderer": "^16.11.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"standard": "^14.3.1",
|
||||
"stylelint": "^11.1.1",
|
||||
"stylelint-config-standard": "^19.0.0",
|
||||
"ts-jest": "^24.1.0",
|
||||
"tslint": "^5.20.0",
|
||||
"tslint-microsoft-contrib": "^6.2.0",
|
||||
"tslint-react": "^4.1.0",
|
||||
"typescript": "^3.6.4"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare module 'fix-path';
|
||||
declare module 'namor';
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
|
||||
// Type-only-import
|
||||
import { App as AppType } from './renderer/app';
|
||||
import { EditorId } from './interfaces';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ElectronFiddle: {
|
||||
app: AppType;
|
||||
contentChangeListeners: Array<any>,
|
||||
editors: Record<EditorId, MonacoType.editor.IStandaloneCodeEditor | null>;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
export const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -16,4 +16,5 @@
|
||||
require('./renderer.js')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>`;
|
||||
|
||||
@@ -1,60 +1,56 @@
|
||||
require('update-electron-app')();
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
|
||||
app.quit();
|
||||
}
|
||||
export const main = `// Modules to control application life and create native browser window
|
||||
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;
|
||||
let mainWindow
|
||||
|
||||
const createWindow = () => {
|
||||
function createWindow () {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 900,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
acceptFirstMouse: true
|
||||
});
|
||||
mainWindow = new BrowserWindow({width: 800, height: 600})
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile('./static/index.html');
|
||||
mainWindow.loadURL(url.format({
|
||||
protocol: 'file',
|
||||
slashes: true,
|
||||
pathname: path.resolve(app.getAppPath(), 'index.html')
|
||||
}))
|
||||
|
||||
// Open the DevTools.
|
||||
// mainWindow.webContents.openDevTools();
|
||||
// mainWindow.webContents.openDevTools()
|
||||
|
||||
// Emitted when the window is closed.
|
||||
mainWindow.on('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;
|
||||
});
|
||||
};
|
||||
mainWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow);
|
||||
app.on('ready', createWindow)
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
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();
|
||||
app.quit()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
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) {
|
||||
createWindow();
|
||||
createWindow()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and import them here.
|
||||
// code. You can also put them in separate files and require them here.`;
|
||||
@@ -1,4 +1,4 @@
|
||||
// Modules to control application life and create native browser window
|
||||
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
|
||||
@@ -7,7 +7,13 @@ let mainWindow
|
||||
|
||||
function createWindow () {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({width: 800, height: 600})
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
})
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile('index.html')
|
||||
@@ -47,4 +53,4 @@ app.on('activate', function () {
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
// code. You can also put them in separate files and require them here.`;
|
||||
@@ -0,0 +1,3 @@
|
||||
export const renderer = `// This file is required by the index.html file and will
|
||||
// be executed in the renderer process for that window.
|
||||
// All of the Node.js APIs are available in this process.`;
|
||||
+77
-17
@@ -1,27 +1,87 @@
|
||||
export interface StringMap<T> {
|
||||
[x: string]: T;
|
||||
export type WindowNames = 'main';
|
||||
|
||||
export type Files = Map<string, string>;
|
||||
|
||||
export type FileTransform = (files: Files) => Promise<Files>;
|
||||
|
||||
export enum ElectronVersionState {
|
||||
ready = 'ready',
|
||||
downloading = 'downloading',
|
||||
unknown = 'unknown'
|
||||
}
|
||||
|
||||
export type ElectronVersionState = 'downloading' | 'ready' | 'unknown';
|
||||
|
||||
export interface GitHubVersion {
|
||||
url: string;
|
||||
assets_url: string;
|
||||
html_url: string;
|
||||
tag_name: string;
|
||||
target_commitish: string;
|
||||
name: string;
|
||||
prerelease: boolean;
|
||||
created_at: string;
|
||||
published_at: string;
|
||||
body: string;
|
||||
export enum ElectronVersionSource {
|
||||
remote = 'remote',
|
||||
local = 'local'
|
||||
}
|
||||
export interface NpmVersion {
|
||||
version: string;
|
||||
name?: string;
|
||||
localPath?: string;
|
||||
}
|
||||
|
||||
export interface ElectronVersion extends GitHubVersion {
|
||||
export interface EditorValues {
|
||||
main: string;
|
||||
renderer: string;
|
||||
html: string;
|
||||
preload: string;
|
||||
package?: string;
|
||||
}
|
||||
|
||||
export interface ElectronVersion extends NpmVersion {
|
||||
state: ElectronVersionState;
|
||||
source: ElectronVersionSource;
|
||||
}
|
||||
|
||||
export interface SetFiddleOptions {
|
||||
filePath?: string;
|
||||
templateName?: string;
|
||||
gistId?: string;
|
||||
}
|
||||
|
||||
export interface OutputEntry {
|
||||
text: string;
|
||||
timestamp: number;
|
||||
}
|
||||
isNotPre?: boolean;
|
||||
}
|
||||
|
||||
export interface OutputOptions {
|
||||
bypassBuffer?: boolean;
|
||||
isNotPre?: boolean;
|
||||
}
|
||||
|
||||
export interface WarningDialogTexts {
|
||||
ok?: string;
|
||||
cancel?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Templates {
|
||||
[index: string]: string | Templates;
|
||||
}
|
||||
|
||||
// Editors
|
||||
export const enum EditorId {
|
||||
'main' = 'main',
|
||||
'renderer' = 'renderer',
|
||||
'html' = 'html',
|
||||
'preload' = 'preload'
|
||||
}
|
||||
|
||||
// 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 ];
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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',
|
||||
FS_SAVE_FIDDLE = 'FS_SAVE_FIDDLE',
|
||||
FS_SAVE_FIDDLE_AS = 'FS_SAVE_FIDDLE_AS',
|
||||
FS_SAVE_FIDDLE_GIST = 'FS_SAVE_FIDDLE_GIST',
|
||||
FS_SAVE_FIDDLE_FORGE = 'FS_SAVE_FIDDLE_FORGE',
|
||||
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',
|
||||
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',
|
||||
}
|
||||
|
||||
export const ipcMainEvents = [
|
||||
IpcEvents.FS_SAVE_FIDDLE_DIALOG,
|
||||
IpcEvents.FS_SAVE_FIDDLE,
|
||||
IpcEvents.SHOW_WARNING_DIALOG,
|
||||
IpcEvents.SHOW_CONFIRMATION_DIALOG,
|
||||
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG
|
||||
];
|
||||
|
||||
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,
|
||||
IpcEvents.FS_SAVE_FIDDLE,
|
||||
IpcEvents.FS_SAVE_FIDDLE_AS,
|
||||
IpcEvents.FS_SAVE_FIDDLE_FORGE,
|
||||
IpcEvents.FS_SAVE_FIDDLE_GIST,
|
||||
IpcEvents.SHOW_WELCOME_TOUR,
|
||||
IpcEvents.CLEAR_CONSOLE,
|
||||
IpcEvents.LOAD_LOCAL_VERSION_FOLDER,
|
||||
IpcEvents.BISECT_COMMANDS_TOGGLE
|
||||
];
|
||||
|
||||
export const WEBCONTENTS_READY_FOR_IPC_SIGNAL = 'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
|
||||
@@ -0,0 +1,45 @@
|
||||
@import (inline) "../../node_modules/@blueprintjs/core/lib/css/blueprint.css";
|
||||
@import (inline) "../../node_modules/@blueprintjs/select/lib/css/blueprint-select.css";
|
||||
@import (inline) "../../node_modules/@blueprintjs/icons/lib/css/blueprint-icons.css";
|
||||
@import (inline) "../../node_modules/@blueprintjs/core/lib/less/variables.less";
|
||||
|
||||
// Override some of the colors
|
||||
.fiddle.bp3-dark {
|
||||
.bp3-menu,
|
||||
.bp3-popover .bp3-popover-content {
|
||||
background-color: @background-1;
|
||||
}
|
||||
|
||||
.bp3-popover .bp3-popover-arrow-fill {
|
||||
fill: @background-1;
|
||||
}
|
||||
|
||||
.bp3-button:not([class*="bp3-intent-"]) {
|
||||
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;
|
||||
}
|
||||
|
||||
.bp3-dialog {
|
||||
background-color: @background-4;
|
||||
|
||||
.bp3-dialog-header {
|
||||
background-color: @background-3;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-running-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.darwin {
|
||||
.chrome {
|
||||
text-align: center;
|
||||
background-color: @background-1;
|
||||
border-bottom: @border;
|
||||
-webkit-app-region: drag;
|
||||
height: 24px;
|
||||
font-size: 1.4rem;
|
||||
line-height: 15px;
|
||||
|
||||
small,
|
||||
p,
|
||||
span {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
@import "../variables.less";
|
||||
|
||||
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);
|
||||
background-color: @background-3;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.commands {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
font-family: @fonts-common;
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
-webkit-app-region: no-drag;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 10px;
|
||||
|
||||
.bp3-button-group {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-control-group {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.address-bar {
|
||||
width: 420px;
|
||||
transition: width 0.4s;
|
||||
|
||||
input,
|
||||
span {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
span,
|
||||
input {
|
||||
border-color: @foreground-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
@import "../variables.less";
|
||||
|
||||
.dialogs {
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
pointer-events: none;
|
||||
|
||||
* {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.darwin {
|
||||
.dialogs {
|
||||
padding-top: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: @background-3;
|
||||
border: @border;
|
||||
color: @text-color-1;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
animation: slide-up 0.4s ease;
|
||||
|
||||
p {
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
&-buttons {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
&.centered {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
left: calc(50% - (300px / 2));
|
||||
top: 100px;
|
||||
}
|
||||
|
||||
&.arrow::after,
|
||||
&.arrow::before {
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.arrow::after {
|
||||
border-color: rgba(136, 183, 213, 0);
|
||||
border-width: 9px;
|
||||
}
|
||||
|
||||
&.arrow::before {
|
||||
border-color: rgba(194, 225, 245, 0);
|
||||
border-width: 10px;
|
||||
}
|
||||
|
||||
&.arrow-top::after,
|
||||
&.arrow-top::before {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
&.arrow-top::after {
|
||||
border-bottom-color: @background-3;
|
||||
margin-left: -9px;
|
||||
}
|
||||
|
||||
&.arrow-top::before {
|
||||
border-bottom-color: @border-color-1;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
&.arrow-bottom::after,
|
||||
&.arrow-bottom::before {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
&.arrow-bottom::after {
|
||||
border-top-color: @background-3;
|
||||
margin-left: -9px;
|
||||
}
|
||||
|
||||
&.arrow-bottom::before {
|
||||
border-top-color: @border-color-1;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
&.arrow-left::after,
|
||||
&.arrow-left::before {
|
||||
right: 100%;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
&.arrow-left::after {
|
||||
border-right-color: @background-3;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
&.arrow-left::before {
|
||||
border-right-color: @border-color-1;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
&.arrow-right::after,
|
||||
&.arrow-right::before {
|
||||
left: 100%;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
&.arrow-right::after {
|
||||
border-left-color: @background-3;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
&.arrow-right::before {
|
||||
border-left-color: @border-color-1;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
button {
|
||||
color: #1e2527;
|
||||
float: right;
|
||||
margin-left: 9px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
color: rgba(159, 234, 249, 0.5);
|
||||
height: 28px;
|
||||
margin-top: 10px;
|
||||
background-color: @background-1;
|
||||
border: 1px solid @border-color-2;
|
||||
outline: 0 solid transparent;
|
||||
|
||||
&:focus {
|
||||
border-color: @foreground-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialogDrop {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
pointer-events: all;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dialog-add-version {
|
||||
.bp3-file-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bp3-callout {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
background-color: white;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.mosaic-tile {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
@import "../variables.less";
|
||||
|
||||
.fiddle {
|
||||
.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);
|
||||
}
|
||||
|
||||
.mosaic-window-toolbar.draggable:hover {
|
||||
box-shadow: inset 0 0 100px 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mosaic-window-title {
|
||||
color: var(--text-color-3);
|
||||
}
|
||||
|
||||
button.mosaic-default-control {
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin: 5px;
|
||||
background-color: #314246;
|
||||
border: 0.1rem solid #222b35;
|
||||
}
|
||||
|
||||
button.mosaic-default-control:hover {
|
||||
background-color: #131d1f;
|
||||
}
|
||||
|
||||
.mosaic-window-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
overflow: visible;
|
||||
z-index: 4;
|
||||
background-color: @background-4;
|
||||
}
|
||||
|
||||
.mosaic {
|
||||
height: 100vh;
|
||||
|
||||
&.mosaic-blueprint-theme {
|
||||
background: #bdbdbd15;
|
||||
}
|
||||
}
|
||||
|
||||
.editorContainer,
|
||||
.monaco-editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.editorContainer {
|
||||
background: @background-1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@import "../variables.less";
|
||||
|
||||
.output {
|
||||
font-size: 12px;
|
||||
font-family: @fonts-common;
|
||||
font-weight: 600;
|
||||
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: 100%;
|
||||
overflow-y: scroll;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
white-space: pre;
|
||||
padding-left: 5.6em;
|
||||
text-indent: -5.6em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
@import "../variables.less";
|
||||
|
||||
.settings {
|
||||
background: @background-3;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
cursor: initial;
|
||||
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
color: @text-color-1;
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.bp3-divider {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.bp3-callout {
|
||||
max-width: 850px;
|
||||
}
|
||||
|
||||
.electron-versions-table {
|
||||
width: 100%;
|
||||
|
||||
.action {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
overflow: scroll;
|
||||
color: @text-color-3;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settings-electron {
|
||||
.advanced-options-buttons {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
> div {
|
||||
margin-right: 1rem;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> div:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.versions-to-show {
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid @border-color-1;
|
||||
padding-bottom: 1.5rem;
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-of-type {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-close {
|
||||
position: fixed;
|
||||
right: 30px;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-radius: 15px;
|
||||
color: var(--text-color-3);
|
||||
|
||||
&:hover {
|
||||
color: @text-color-1;
|
||||
}
|
||||
|
||||
svg {
|
||||
background: @background-3;
|
||||
box-shadow: 0 0 12px 6px @background-3;
|
||||
}
|
||||
}
|
||||
|
||||
.contributors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.contributor {
|
||||
width: 320px;
|
||||
height: 120px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 10px;
|
||||
margin: 0 10px 10px 0;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
h5,
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
background-size: contain;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.details {
|
||||
max-width: 190px;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-menu {
|
||||
height: 100%;
|
||||
width: 250px;
|
||||
background-color: @background-1;
|
||||
user-select: none;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 1rem;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: @border;
|
||||
color: var(--text-color-1);
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.tour {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.tour-portal {
|
||||
z-index: 1000;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: unset;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: @background-1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fiddle {
|
||||
*::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-corner {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
.drag {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
margin-right: 5px;
|
||||
color: var(--text-color-1);
|
||||
opacity: 0.7;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
#editors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.editor {
|
||||
flex-grow: 1;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.resize {
|
||||
width: 6px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
margin-left: 9px;
|
||||
margin-right: 0;
|
||||
cursor: col-resize;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#runner {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#runner:not(:empty) {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.editorTitle span {
|
||||
width: 33%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* The CSS below is a modified version of the original, increasing the
|
||||
* FPS from ~11 during scrolling to 60 by reducing the number of
|
||||
* style calculations.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016 Palantir Technologies, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
.mosaic {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mosaic,
|
||||
.mosaic > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mosaic .mosaic-zero-state {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mosaic .mosaic-zero-state .pt-non-ideal-state-icon .pt-icon {
|
||||
font-size: 120px;
|
||||
}
|
||||
|
||||
.mosaic-root {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
.mosaic-split {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mosaic-split:hover {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.mosaic-split .mosaic-split-line {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.mosaic-split.-row {
|
||||
margin-left: -3px;
|
||||
width: 6px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.mosaic-split.-row .mosaic-split-line {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 3px;
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
.mosaic-split.-column {
|
||||
margin-top: -3px;
|
||||
height: 6px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.mosaic-split.-column .mosaic-split-line {
|
||||
top: 3px;
|
||||
bottom: 3px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mosaic-tile {
|
||||
position: absolute;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.mosaic-tile > * {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mosaic-drop-target {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mosaic-drop-target.drop-target-hover .drop-target-container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mosaic-drop-target.mosaic > .drop-target-container .drop-target.left {
|
||||
right: calc(100% - 10px );
|
||||
}
|
||||
|
||||
.mosaic-drop-target.mosaic > .drop-target-container .drop-target.right {
|
||||
left: calc(100% - 10px );
|
||||
}
|
||||
|
||||
.mosaic-drop-target.mosaic > .drop-target-container .drop-target.bottom {
|
||||
top: calc(100% - 10px );
|
||||
}
|
||||
|
||||
.mosaic-drop-target.mosaic > .drop-target-container .drop-target.top {
|
||||
bottom: calc(100% - 10px );
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container.-dragging {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 2px solid black;
|
||||
opacity: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target.left {
|
||||
right: calc(100% - 30% );
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target.right {
|
||||
left: calc(100% - 30% );
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target.bottom {
|
||||
top: calc(100% - 30% );
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target.top {
|
||||
bottom: calc(100% - 30% );
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.left {
|
||||
right: calc(100% - 50% );
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.right {
|
||||
left: calc(100% - 50% );
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.bottom {
|
||||
top: calc(100% - 50% );
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container .drop-target.drop-target-hover.top {
|
||||
bottom: calc(100% - 50% );
|
||||
}
|
||||
|
||||
.mosaic-window,
|
||||
.mosaic-preview {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-toolbar,
|
||||
.mosaic-preview .mosaic-window-toolbar {
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
height: 30px;
|
||||
background: white;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-toolbar.draggable,
|
||||
.mosaic-preview .mosaic-window-toolbar.draggable {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-title,
|
||||
.mosaic-preview .mosaic-window-title {
|
||||
padding-left: 15px;
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-controls,
|
||||
.mosaic-preview .mosaic-window-controls {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-controls .separator,
|
||||
.mosaic-preview .mosaic-window-controls .separator {
|
||||
height: 20px;
|
||||
border-left: 1px solid black;
|
||||
margin: 5px 4px;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-body,
|
||||
.mosaic-preview .mosaic-window-body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 0;
|
||||
background: white;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-additional-actions-bar,
|
||||
.mosaic-preview .mosaic-window-additional-actions-bar {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 0;
|
||||
bottom: initial;
|
||||
left: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-additional-actions-bar .pt-button,
|
||||
.mosaic-preview .mosaic-window-additional-actions-bar .pt-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-additional-actions-bar .pt-button::after,
|
||||
.mosaic-preview .mosaic-window-additional-actions-bar .pt-button::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-window-body-overlay,
|
||||
.mosaic-preview .mosaic-window-body-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
background: white;
|
||||
display: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mosaic-window.additional-controls-open .mosaic-window-additional-actions-bar,
|
||||
.mosaic-preview.additional-controls-open .mosaic-window-additional-actions-bar {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.mosaic-window.additional-controls-open .mosaic-window-body-overlay,
|
||||
.mosaic-preview.additional-controls-open .mosaic-window-body-overlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-preview,
|
||||
.mosaic-preview .mosaic-preview {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
border: 1px solid black;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-preview .mosaic-window-body,
|
||||
.mosaic-preview .mosaic-preview .mosaic-window-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-preview h4,
|
||||
.mosaic-preview .mosaic-preview h4 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mosaic-window .mosaic-preview .pt-icon,
|
||||
.mosaic-preview .mosaic-preview .pt-icon {
|
||||
font-size: 72px;
|
||||
}
|
||||
|
||||
.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.pt-icon-cross::before {
|
||||
content: 'Close';
|
||||
}
|
||||
|
||||
.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.pt-icon-add-column-right::before {
|
||||
content: 'Split';
|
||||
}
|
||||
|
||||
.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.pt-icon-exchange::before {
|
||||
content: 'Replace';
|
||||
}
|
||||
|
||||
.mosaic:not(.mosaic-blueprint-theme) .mosaic-default-control.pt-icon-maximize::before {
|
||||
content: 'Expand';
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Blueprint
|
||||
@import "blueprint.less";
|
||||
|
||||
// Variables, also imported in each file
|
||||
@import "variables.less";
|
||||
@import "main.less";
|
||||
@import "container.less";
|
||||
@import "mosaic-vendor.less";
|
||||
|
||||
// Components
|
||||
@import "components/commands.less";
|
||||
@import "components/output.less";
|
||||
@import "components/dialogs.less";
|
||||
@import "components/mosaic.less";
|
||||
@import "components/settings.less";
|
||||
@import "components/chrome-mac.less";
|
||||
@import "components/editors.less";
|
||||
@import "components/tour.less";
|
||||
@import "components/show-me.less";
|
||||
@@ -0,0 +1,25 @@
|
||||
// We'll take these values from the theme.
|
||||
|
||||
// Colors
|
||||
@foreground-1: var(--foreground-1);
|
||||
@foreground-2: var(--foreground-2);
|
||||
@foreground-3: var(--foreground-3);
|
||||
@background-4: var(--background-4);
|
||||
@background-3: var(--background-3);
|
||||
@background-2: var(--background-2);
|
||||
@background-1: var(--background-1);
|
||||
@border-color-2: var(--border-color-2);
|
||||
@border-color-1: var(--border-color-1);
|
||||
@border: var(--border);
|
||||
@text-color-1: var(--text-color-1);
|
||||
@text-color-2: var(--text-color-2);
|
||||
@text-color-3: var(--text-color-3);
|
||||
|
||||
// Dynamically created
|
||||
@button-text-color: var(--button-text-color);
|
||||
|
||||
// Fonts
|
||||
@fonts-common: var(--fonts-common);
|
||||
|
||||
// Lengths
|
||||
@header-height: 50px;
|
||||
@@ -0,0 +1,165 @@
|
||||
import { BrowserWindow, ContextMenuParams, Menu, MenuItemConstructorOptions } from 'electron';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
import { ipcMainManager } from './ipc';
|
||||
|
||||
/**
|
||||
* Returns items related to running the current fiddle.
|
||||
*
|
||||
* @returns {Array<MenuItemConstructorOptions>}
|
||||
*/
|
||||
export function getRunItems(): Array<MenuItemConstructorOptions> {
|
||||
return [
|
||||
{
|
||||
id: 'run',
|
||||
label: 'Run Fiddle',
|
||||
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
|
||||
},
|
||||
{
|
||||
id: 'clear_console',
|
||||
label: 'Clear Console',
|
||||
click: () => ipcMainManager.send(IpcEvents.CLEAR_CONSOLE)
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Possibly returns items interacting with the Monaco editor.
|
||||
* Our check for "are we in the Monaco editor" is pretty crude -
|
||||
* we just assume that we are if we can paste text.
|
||||
*
|
||||
* @param {BrowserWindow} browserWindow
|
||||
* @param {ContextMenuParams} { x, y }
|
||||
* @returns {Array<MenuItemConstructorOptions>}
|
||||
*/
|
||||
export function getMonacoItems(
|
||||
{ pageURL, editFlags }: ContextMenuParams
|
||||
): Array<MenuItemConstructorOptions> {
|
||||
if (!editFlags.canPaste || !/.*index\.html(#?)$/.test(pageURL || '')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'go_to_definition',
|
||||
label: 'Go to Definition',
|
||||
click() {
|
||||
const cmd = [ 'editor.action.goToDeclaration' ];
|
||||
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'peek_definition',
|
||||
label: 'Peek Definition',
|
||||
click() {
|
||||
const cmd = [ 'editor.action.previewDeclaration' ];
|
||||
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'references',
|
||||
label: 'Find References',
|
||||
click() {
|
||||
const cmd = [ 'editor.action.referenceSearch.trigger' ];
|
||||
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
id: 'palette',
|
||||
label: 'Command Palette',
|
||||
click() {
|
||||
const cmd = [ 'editor.action.quickCommand' ];
|
||||
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
id: 'format_document',
|
||||
label: 'Format Document',
|
||||
click() {
|
||||
const cmd = [ 'editor.action.formatDocument' ];
|
||||
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'format_selection',
|
||||
label: 'Format Selection',
|
||||
click() {
|
||||
const cmd = [ 'editor.action.formatSelection' ];
|
||||
ipcMainManager.send(IpcEvents.MONACO_EXECUTE_COMMAND, cmd);
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Possibly returns the `Inspect Element` item.
|
||||
*
|
||||
* @param {BrowserWindow} browserWindow
|
||||
* @param {ContextMenuParams} { x, y }
|
||||
* @returns {Array<MenuItemConstructorOptions>}
|
||||
*/
|
||||
export function getInspectItems(
|
||||
browserWindow: BrowserWindow, { x, y }: ContextMenuParams
|
||||
): Array<MenuItemConstructorOptions> {
|
||||
if (!isDevMode()) return [];
|
||||
|
||||
return [{
|
||||
id: 'inspect',
|
||||
label: 'Inspect Element',
|
||||
click: () => {
|
||||
browserWindow.webContents.inspectElement(x, y);
|
||||
|
||||
try {
|
||||
if (browserWindow.webContents.isDevToolsOpened()) {
|
||||
browserWindow.webContents.devToolsWebContents.focus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Tried to focus dev tools, but failed`, { error });
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a context menu for a given BrowserWindow
|
||||
*
|
||||
* @param {BrowserWindow} browserWindow
|
||||
*/
|
||||
export function createContextMenu(browserWindow: BrowserWindow) {
|
||||
browserWindow.webContents.on('context-menu', (_event, props) => {
|
||||
const { editFlags } = props;
|
||||
|
||||
const template: Array<MenuItemConstructorOptions> = [
|
||||
...getRunItems(),
|
||||
...getMonacoItems(props),
|
||||
{
|
||||
id: 'cut',
|
||||
label: 'Cut',
|
||||
role: 'cut',
|
||||
enabled: editFlags.canCut
|
||||
}, {
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
role: 'copy',
|
||||
enabled: editFlags.canCopy
|
||||
}, {
|
||||
id: 'paste',
|
||||
label: 'Paste',
|
||||
role: 'paste',
|
||||
enabled: editFlags.canPaste
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
...getInspectItems(browserWindow, props)
|
||||
];
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
menu.popup({});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { dialog, IpcMainEvent } from 'electron';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { ipcMainManager } from './ipc';
|
||||
import { getOrCreateMainWindow } from './windows';
|
||||
|
||||
/**
|
||||
* Listens to IPC events related to dialogs and message boxes
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a warning dialog
|
||||
*
|
||||
* @param {Electron.MessageBoxOptions} args
|
||||
*/
|
||||
function showWarningDialog(args: Electron.MessageBoxOptions) {
|
||||
dialog.showMessageBox(getOrCreateMainWindow(), {
|
||||
type: 'warning',
|
||||
...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]]);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { dialog } from 'electron';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the "Open Fiddle" dialog and forwards
|
||||
* the path to the renderer
|
||||
*/
|
||||
export async function showOpenDialog() {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
title: 'Open Fiddle',
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
|
||||
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 async function showSaveDialog(event?: IpcEvents, as?: string) {
|
||||
// We want to save to a folder, so we'll use an open dialog here
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
buttonLabel: 'Save here',
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
title: `Save Fiddle${as ? ` as ${as}` : ''}`
|
||||
});
|
||||
|
||||
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] ]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a folder designated for saving is empty
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function ensureSaveTargetEmpty(filePath: string): Promise<boolean> {
|
||||
const targetPaths = [
|
||||
path.join(filePath, INDEX_HTML_NAME),
|
||||
path.join(filePath, RENDERER_JS_NAME),
|
||||
path.join(filePath, MAIN_JS_NAME),
|
||||
path.join(filePath, PACKAGE_NAME)
|
||||
];
|
||||
|
||||
let noFilesOrOverwriteGranted = true;
|
||||
|
||||
for (const targetPath of targetPaths) {
|
||||
if (fs.existsSync(targetPath) && noFilesOrOverwriteGranted) {
|
||||
noFilesOrOverwriteGranted = await confirmFileOverwrite(targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
return noFilesOrOverwriteGranted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops open a confirmation dialog, asking the user if they really
|
||||
* want to overwrite an existing file
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function confirmFileOverwrite(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: [ 'Cancel', 'Yes' ],
|
||||
message: 'Overwrite files?',
|
||||
detail: `The file ${filePath} already exists. Do you want to overwrite it?`,
|
||||
});
|
||||
|
||||
return !!result;
|
||||
} catch (error) {
|
||||
// Let's not overwrite files. We'd rather crash.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { IpcEvents, ipcMainEvents, WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
|
||||
import { getOrCreateMainWindow } from './windows';
|
||||
|
||||
/**
|
||||
* The main purpose of this class is to be the central
|
||||
* gathering place for IPC calls the main process sends
|
||||
* or listens to.
|
||||
*
|
||||
* @class IpcManager
|
||||
* @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.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an IPC message to an instance of Electron.WebContents.
|
||||
* If none is specified, we'll automatically go with the main window.
|
||||
*
|
||||
* @param {IpcEvents} channel
|
||||
* @param {Array<any>} args
|
||||
* @param {Electron.WebContents} [target]
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export const ipcMainManager = new IpcMainManager();
|
||||
@@ -0,0 +1,83 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
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 { listenForProtocolHandler, setupProtocolHandler } from './protocol';
|
||||
import { shouldQuit } from './squirrel';
|
||||
import { setupUpdates } from './update';
|
||||
import { getOrCreateMainWindow } from './windows';
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
setupMenu();
|
||||
setupProtocolHandler();
|
||||
setupFileListeners();
|
||||
setupUpdates();
|
||||
setupDialogs();
|
||||
setupDevTools();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the "before-quit" event
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function onBeforeQuit() {
|
||||
(global as any).isQuitting = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@@ -0,0 +1,284 @@
|
||||
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
|
||||
|
||||
import { Templates } from '../interfaces';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { SHOW_ME_TEMPLATES } from '../templates';
|
||||
import { showOpenDialog, showSaveDialog } from './files';
|
||||
import { ipcMainManager } from './ipc';
|
||||
import { createMainWindow } from './windows';
|
||||
|
||||
/**
|
||||
* Is the passed object a constructor for an Electron Menu?
|
||||
*
|
||||
* @param {(Array<Electron.MenuItemConstructorOptions> | Electron.Menu)} [submenu]
|
||||
* @returns {submenu is Array<Electron.MenuItemConstructorOptions>}
|
||||
*/
|
||||
function isSubmenu(
|
||||
submenu?: Array<MenuItemConstructorOptions> | Menu
|
||||
): submenu is Array<MenuItemConstructorOptions> {
|
||||
return !!submenu && Array.isArray(submenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns additional items for the help menu
|
||||
*
|
||||
* @returns {Array<Electron.MenuItemConstructorOptions>}
|
||||
*/
|
||||
function getHelpItems(): Array<MenuItemConstructorOptions> {
|
||||
return [
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Show Welcome Tour',
|
||||
click() {
|
||||
ipcMainManager.send(IpcEvents.SHOW_WELCOME_TOUR);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator: 'CmdOrCtrl+Option+i',
|
||||
click() {
|
||||
const browserWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
if (browserWindow && !browserWindow.isDestroyed()) {
|
||||
browserWindow.webContents.openDevTools({ mode: 'bottom' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Open Fiddle Repository...',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/electron/fiddle');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Open Electron Repository...',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/electron/electron');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Open Electron Issue Tracker...',
|
||||
click() {
|
||||
shell.openExternal('https://github.com/electron/electron/issues');
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Depending on the OS, the `Preferences` either go into the `Fiddle`
|
||||
* menu (macOS) or under `File` (Linux, Windows)
|
||||
*
|
||||
* @returns {Array<Electron.MenuItemConstructorOptions>}
|
||||
*/
|
||||
function getPreferencesItems(): Array<MenuItemConstructorOptions> {
|
||||
return [
|
||||
{
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Preferences',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
click() {
|
||||
ipcMainManager.send(IpcEvents.OPEN_SETTINGS);
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Exit items
|
||||
*
|
||||
* @returns {Array<Electron.MenuItemConstructorOptions>}
|
||||
*/
|
||||
function getQuitItems(): Array<MenuItemConstructorOptions> {
|
||||
return [
|
||||
{
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Exit',
|
||||
accelerator: 'Ctrl+Q,',
|
||||
click: app.quit
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top-level "File" menu
|
||||
*
|
||||
* @returns {Array<Electron.MenuItemConstructorOptions>}
|
||||
*/
|
||||
function getTasksMenu(): MenuItemConstructorOptions {
|
||||
const tasksMenu: Array<MenuItemConstructorOptions> = [
|
||||
{
|
||||
label: 'Run Fiddle...',
|
||||
accelerator: 'F5',
|
||||
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
|
||||
},
|
||||
{
|
||||
label: 'Package Fiddle...',
|
||||
click: () => ipcMainManager.send(IpcEvents.FIDDLE_PACKAGE)
|
||||
},
|
||||
{
|
||||
label: 'Make installers for Fiddle...',
|
||||
click: () => ipcMainManager.send(IpcEvents.FIDDLE_MAKE)
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
label: 'Tasks',
|
||||
submenu: tasksMenu
|
||||
};
|
||||
}
|
||||
|
||||
function getShowMeMenuItem(key: string, item: string | Templates): MenuItemConstructorOptions {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
label: key,
|
||||
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [key])
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: key,
|
||||
submenu: Object.keys(item).map((subkey) => {
|
||||
return getShowMeMenuItem(subkey, item[subkey]);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top-level "File" menu
|
||||
*
|
||||
* @returns {Array<Electron.MenuItemConstructorOptions>}
|
||||
*/
|
||||
function getFileMenu(): MenuItemConstructorOptions {
|
||||
const fileMenu: Array<MenuItemConstructorOptions> = [
|
||||
{
|
||||
label: 'New Fiddle',
|
||||
click: () => ipcMainManager.send(IpcEvents.FS_NEW_FIDDLE),
|
||||
accelerator: 'CmdOrCtrl+N'
|
||||
}, {
|
||||
label: 'New Window',
|
||||
click: () => createMainWindow(),
|
||||
accelerator: 'CmdOrCtrl+Shift+N'
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Open',
|
||||
click: showOpenDialog,
|
||||
accelerator: 'CmdOrCtrl+O'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
click: () => ipcMainManager.send(IpcEvents.FS_SAVE_FIDDLE),
|
||||
accelerator: 'CmdOrCtrl+S'
|
||||
},
|
||||
{
|
||||
label: 'Save as',
|
||||
click: () => showSaveDialog(IpcEvents.FS_SAVE_FIDDLE),
|
||||
accelerator: 'CmdOrCtrl+Shift+S'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Publish to Gist',
|
||||
click: () => ipcMainManager.send(IpcEvents.FS_SAVE_FIDDLE_GIST),
|
||||
},
|
||||
{
|
||||
label: 'Save as Forge Project',
|
||||
click: () => showSaveDialog(IpcEvents.FS_SAVE_FIDDLE_FORGE, 'Forge Project')
|
||||
}
|
||||
];
|
||||
|
||||
// macOS has these items in the "Fiddle" menu
|
||||
if (process.platform !== 'darwin') {
|
||||
fileMenu.splice(fileMenu.length, 0, ...getPreferencesItems(), ...getQuitItems());
|
||||
}
|
||||
|
||||
return {
|
||||
label: 'File',
|
||||
submenu: fileMenu
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the app's window menu.
|
||||
*/
|
||||
export function setupMenu() {
|
||||
// Get template for default menu
|
||||
const defaultMenu = require('electron-default-menu');
|
||||
const menu = (defaultMenu(app, shell) as Array<MenuItemConstructorOptions>)
|
||||
.map((item) => {
|
||||
const { label } = item;
|
||||
|
||||
// Append the "Settings" item
|
||||
if (
|
||||
process.platform === 'darwin'
|
||||
&& label === app.getName()
|
||||
&& isSubmenu(item.submenu)
|
||||
) {
|
||||
item.submenu.splice(2, 0, ...getPreferencesItems());
|
||||
}
|
||||
|
||||
// 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
|
||||
item.submenu.push({ type: 'separator' }, {
|
||||
label: 'Toggle Soft Wrap',
|
||||
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['wordWrap']),
|
||||
});
|
||||
item.submenu.push({ type: 'separator' }, {
|
||||
label: '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',
|
||||
});
|
||||
}
|
||||
|
||||
// Append items to "Help"
|
||||
if (label === 'Help' && isSubmenu(item.submenu)) {
|
||||
item.submenu = getHelpItems();
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
menu.splice(
|
||||
process.platform === 'darwin' ? 1 : 0,
|
||||
0,
|
||||
getFileMenu()
|
||||
);
|
||||
|
||||
menu.splice(menu.length - 1, 0, getTasksMenu(), getShowMeMenu());
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { app } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
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';
|
||||
const squirrelPath = path.resolve(path.dirname(process.execPath), '..', 'electron-fiddle.exe');
|
||||
|
||||
const handlePotentialProtocolLaunch = (url: string) => {
|
||||
if (!app.isReady()) {
|
||||
app.once('ready', () => handlePotentialProtocolLaunch(url));
|
||||
return;
|
||||
}
|
||||
|
||||
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.on('open-url', (_, url) => {
|
||||
if (url.startsWith(`${PROTOCOL}://`)) {
|
||||
handlePotentialProtocolLaunch(url);
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
export const setupProtocolHandler = () => {
|
||||
if (process.platform === 'win32' && !fs.existsSync(squirrelPath)) return;
|
||||
if (!app.isDefaultProtocolClient(PROTOCOL, squirrelPath)) {
|
||||
app.setAsDefaultProtocolClient(PROTOCOL, squirrelPath);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export function shouldQuit() {
|
||||
return require('electron-squirrel-startup');
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Sets up the update service
|
||||
*/
|
||||
export function setupUpdates() {
|
||||
// We delay this work by 10s to ensure that the
|
||||
// app doesn't have to worry about updating during launch
|
||||
setTimeout(() => {
|
||||
const updateApp = require('update-electron-app');
|
||||
|
||||
updateApp({
|
||||
repo: 'electron/fiddle',
|
||||
updateInterval: '1 hour',
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { BrowserWindow, shell } from 'electron';
|
||||
import { createContextMenu } from './context-menu';
|
||||
|
||||
// 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> = [];
|
||||
|
||||
/**
|
||||
* Gets default options for the main window
|
||||
*
|
||||
* @returns {Electron.BrowserWindowConstructorOptions}
|
||||
*/
|
||||
export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions {
|
||||
return {
|
||||
width: 1200,
|
||||
height: 900,
|
||||
minHeight: 600,
|
||||
minWidth: 600,
|
||||
titleBarStyle: process.platform === 'darwin' ? 'hidden' : undefined,
|
||||
acceptFirstMouse: true,
|
||||
backgroundColor: '#1d2427',
|
||||
webPreferences: {
|
||||
webviewTag: false,
|
||||
nodeIntegration: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new main window.
|
||||
*
|
||||
* @export
|
||||
* @returns {Electron.BrowserWindow}
|
||||
*/
|
||||
export function createMainWindow(): Electron.BrowserWindow {
|
||||
console.log(`Creating main window`);
|
||||
const browserWindow = new BrowserWindow(getMainWindowOptions());
|
||||
browserWindow.loadFile('./dist/static/index.html');
|
||||
|
||||
browserWindow.webContents.once('dom-ready', () => {
|
||||
browserWindow.show();
|
||||
|
||||
if (browserWindow) {
|
||||
createContextMenu(browserWindow);
|
||||
}
|
||||
});
|
||||
|
||||
browserWindow.on('closed', () => {
|
||||
browserWindows = browserWindows
|
||||
.filter((bw) => browserWindow !== bw);
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('new-window', (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
browserWindows.push(browserWindow);
|
||||
|
||||
return browserWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates the main window, returning it in both cases.
|
||||
*
|
||||
* @returns {Electron.BrowserWindow}
|
||||
*/
|
||||
export function getOrCreateMainWindow(): Electron.BrowserWindow {
|
||||
return BrowserWindow.getFocusedWindow() || browserWindows[0] || createMainWindow();
|
||||
}
|
||||
+213
-82
@@ -1,96 +1,227 @@
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import * as loader from 'monaco-loader';
|
||||
import { observable } from 'mobx';
|
||||
import { when } from 'mobx';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
|
||||
import { mainTheme } from './themes';
|
||||
import { getContent } from './content';
|
||||
import { Header } from './components/header';
|
||||
import { BinaryManager } from './binary';
|
||||
import { ElectronVersion, StringMap, OutputEntry } from '../interfaces';
|
||||
import { arrayToStringMap } from '../utils/array-to-stringmap';
|
||||
import { getKnownVersions } from './versions';
|
||||
import { normalizeVersion } from '../utils/normalize-version';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import {
|
||||
ALL_EDITORS,
|
||||
EditorId,
|
||||
EditorValues,
|
||||
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 { 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';
|
||||
|
||||
const knownVersions = getKnownVersions();
|
||||
const defaultVersion = normalizeVersion(knownVersions[0].tag_name);
|
||||
|
||||
export class AppState {
|
||||
@observable public version: string = defaultVersion;
|
||||
@observable public binaryManager: BinaryManager = new BinaryManager(defaultVersion);
|
||||
@observable public versions: StringMap<ElectronVersion> = arrayToStringMap(knownVersions);
|
||||
@observable public output: Array<OutputEntry> = [];
|
||||
@observable public isConsoleShowing: boolean = false;
|
||||
}
|
||||
|
||||
const appState = new AppState();
|
||||
|
||||
class App {
|
||||
public editors: any = {
|
||||
main: null,
|
||||
renderer: null,
|
||||
html: null
|
||||
};
|
||||
public monaco: any = null;
|
||||
public name = 'test';
|
||||
/**
|
||||
* The top-level class controlling the whole app. This is *not* a React component,
|
||||
* but it does eventually render all components.
|
||||
*
|
||||
* @class App
|
||||
*/
|
||||
export class App {
|
||||
public typeDefDisposable: MonacoType.IDisposable | null = null;
|
||||
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.getEditorValues = this.getEditorValues.bind(this);
|
||||
this.setEditorValues = this.setEditorValues.bind(this);
|
||||
|
||||
this.setup();
|
||||
if (process.platform === 'darwin') {
|
||||
this.touchBarManager = new TouchBarManager(appState);
|
||||
}
|
||||
}
|
||||
|
||||
private async setup() {
|
||||
this.monaco = await loader();
|
||||
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.setWarningDialogTexts({
|
||||
label: `Opening this Fiddle will replace your unsaved changes. Do you want to proceed?`,
|
||||
ok: 'Yes'
|
||||
});
|
||||
this.state.isWarningDialogShowing = true;
|
||||
await when(() => !this.state.isWarningDialogShowing);
|
||||
|
||||
this.createThemes();
|
||||
this.editors.html = this.createEditor('html');
|
||||
this.editors.main = this.createEditor('main');
|
||||
this.editors.renderer = this.createEditor('renderer');
|
||||
|
||||
render(<Header appState={appState} />, document.getElementById('header'));
|
||||
}
|
||||
|
||||
private createThemes() {
|
||||
this.monaco.editor.defineTheme('main', mainTheme);
|
||||
}
|
||||
|
||||
private createEditor(id: string) {
|
||||
if (!this.monaco) throw new Error('Monaco not ready');
|
||||
|
||||
const element = document.getElementById(`editor-${id}`);
|
||||
const language = id === 'html' ? 'html' : 'javascript';
|
||||
const value = getContent(id);
|
||||
|
||||
const options = {
|
||||
language,
|
||||
theme: 'main',
|
||||
automaticLayout: true,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
value
|
||||
};
|
||||
|
||||
return this.monaco.editor.create(element, options);
|
||||
}
|
||||
|
||||
private getValues() {
|
||||
if (!this.editors.html || !this.editors.main || !this.editors.renderer) {
|
||||
throw new Error('Editors not ready');
|
||||
if (!this.state.warningDialogLastResult) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
html: this.editors.html!.getValue(),
|
||||
main: this.editors.main!.getValue(),
|
||||
renderer: this.editors.renderer!.getValue(),
|
||||
package: JSON.stringify({
|
||||
name: this.name,
|
||||
main: './main.js',
|
||||
version: '1.0.0'
|
||||
})
|
||||
// set values once prompt approves
|
||||
await this.setEditorValues(editorValues);
|
||||
|
||||
document.title = getTitle(this.state);
|
||||
this.state.gistId = gistId || '';
|
||||
this.state.localPath = filePath;
|
||||
this.state.templateName = templateName;
|
||||
|
||||
// once loaded, we have a "saved" state
|
||||
this.state.isUnsaved = false;
|
||||
this.setupUnsavedOnChangeListener();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the values on all three editors.
|
||||
*
|
||||
* @param {EditorValues} values
|
||||
* @param {warn} warn - Should we warn before overwriting unsaved data?
|
||||
*/
|
||||
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) && backup.model) {
|
||||
// The editor does not exist, attempt to set it on the backup
|
||||
backup.model.setValue(values[name]!);
|
||||
} else if (editor && editor.setValue) {
|
||||
// The editor exists, set the value directly
|
||||
editor.setValue(values[name]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values on all three editors.
|
||||
*
|
||||
* @returns {EditorValues}
|
||||
*/
|
||||
public async getEditorValues(
|
||||
options?: PackageJsonOptions
|
||||
): Promise<EditorValues> {
|
||||
const { ElectronFiddle: fiddle } = window;
|
||||
|
||||
if (!fiddle) {
|
||||
throw new Error('Fiddle not ready');
|
||||
}
|
||||
|
||||
const values: EditorValues = {
|
||||
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(this.state, values, options);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial setup call, loading Monaco and kicking off the React
|
||||
* render process.
|
||||
*/
|
||||
public async setup(): Promise<void | Element | React.Component> {
|
||||
this.setupTheme();
|
||||
|
||||
const React = await import('react');
|
||||
const { render } = await import('react-dom');
|
||||
const { Dialogs } = await import('./components/dialogs');
|
||||
const { OutputEditorsWrapper } = await import(
|
||||
'./components/output-editors-wrapper'
|
||||
);
|
||||
const { Header } = await import('./components/header');
|
||||
|
||||
const className = `${process.platform} container`;
|
||||
const app = (
|
||||
<div className={className}>
|
||||
<Dialogs appState={this.state} />
|
||||
<Header appState={this.state} />
|
||||
<OutputEditorsWrapper appState={this.state} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const rendered = render(app, document.getElementById('app'));
|
||||
|
||||
this.setupResizeListener();
|
||||
|
||||
ipcRenderer.send(WEBCONTENTS_READY_FOR_IPC_SIGNAL);
|
||||
|
||||
// TODO: A timer here is terrible. Let's fix this
|
||||
// and ensure we actually do it once Editors have mounted.
|
||||
setTimeout(() => {
|
||||
this.setupUnsavedOnChangeListener();
|
||||
}, 1500);
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(() => {
|
||||
this.state.isUnsaved = true;
|
||||
disposable.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads theme CSS into the HTML document.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async setupTheme(): Promise<void> {
|
||||
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')) {
|
||||
document.body.classList.add('bp3-dark');
|
||||
} else {
|
||||
document.body.classList.remove('bp3-dark');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to possibly recalculate the layout whenever the window
|
||||
* is resized. This method sets up the listener.
|
||||
*/
|
||||
public setupResizeListener(): void {
|
||||
window.addEventListener('resize', updateEditorLayout);
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).electronFiddle = new App();
|
||||
window.ElectronFiddle = window.ElectronFiddle || {};
|
||||
window.ElectronFiddle.contentChangeListeners =
|
||||
window.ElectronFiddle.contentChangeListeners || [];
|
||||
window.ElectronFiddle.app = window.ElectronFiddle.app || new App();
|
||||
window.ElectronFiddle.app.setup();
|
||||
|
||||
+155
-34
@@ -1,28 +1,83 @@
|
||||
import { promisify } from 'util';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as fsType from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as extract from 'extract-zip';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { userData } from './constants';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { normalizeVersion } from '../utils/normalize-version';
|
||||
import { StringMap } from '../interfaces';
|
||||
import { USER_DATA_PATH } from './constants';
|
||||
import { getOfflineTypeDefinitionPath } from './fetch-types';
|
||||
|
||||
const eDownload = promisify(require('electron-download'));
|
||||
/**
|
||||
* The binary manager takes care of downloading Electron versions
|
||||
*
|
||||
* @export
|
||||
* @class BinaryManager
|
||||
*/
|
||||
export class BinaryManager {
|
||||
public state: Record<string, 'ready' | 'downloading'> = {};
|
||||
|
||||
export class BinaryManager extends EventEmitter {
|
||||
public state: StringMap<'ready' | 'downloading'> = {};
|
||||
public version: string = '2.0.2';
|
||||
/**
|
||||
* 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>}
|
||||
*/
|
||||
public async remove(iVersion: string): Promise<void> {
|
||||
const version = normalizeVersion(iVersion);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
let isDeleted = false;
|
||||
|
||||
constructor(version: string) {
|
||||
super();
|
||||
// utility to re-run removal functions upon failure
|
||||
// due to windows filesystem lockfile jank
|
||||
const rerunner = async (func: () => Promise<void>, counter: number = 1) => {
|
||||
try {
|
||||
await func();
|
||||
} catch (error) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.setup(version);
|
||||
const binaryCleaner = async () => {
|
||||
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;
|
||||
|
||||
isDeleted = true;
|
||||
}
|
||||
};
|
||||
|
||||
const typeDefsCleaner = async () => {
|
||||
await this.removeTypeDefsForVersion(version);
|
||||
};
|
||||
|
||||
await rerunner(binaryCleaner);
|
||||
|
||||
if (isDeleted) {
|
||||
await rerunner(typeDefsCleaner);
|
||||
}
|
||||
}
|
||||
|
||||
public async setup(iVersion: string) {
|
||||
/**
|
||||
* 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') {
|
||||
@@ -30,7 +85,7 @@ export class BinaryManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getIsDownloaded(version)) {
|
||||
if (await this.getIsDownloaded(version)) {
|
||||
console.log(`BinaryManager: Electron ${version} already downloaded.`);
|
||||
this.state[version] = 'ready';
|
||||
return;
|
||||
@@ -41,18 +96,35 @@ export class BinaryManager extends EventEmitter {
|
||||
|
||||
const zipPath = await eDownload({ version });
|
||||
const extractPath = this.getDownloadPath(version);
|
||||
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking`);
|
||||
const electronFiles = await this.unzip(zipPath, extractPath);
|
||||
console.log(electronFiles);
|
||||
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking to ${extractPath}`);
|
||||
|
||||
try {
|
||||
// Ensure the target path is empty
|
||||
await fs.emptyDir(extractPath);
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
public getElectronBinary(version: string) {
|
||||
const platform = os.platform();
|
||||
const dir = this.getDownloadPath(version);
|
||||
|
||||
switch (platform) {
|
||||
/**
|
||||
* 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':
|
||||
@@ -61,12 +133,18 @@ export class BinaryManager extends EventEmitter {
|
||||
case 'win32':
|
||||
return path.join(dir, 'electron.exe');
|
||||
default:
|
||||
throw new Error('Electron builds are not available on platform: ' + platform);
|
||||
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 downloadPath = path.join(userData, 'electron-bin');
|
||||
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 {
|
||||
@@ -74,7 +152,7 @@ export class BinaryManager extends EventEmitter {
|
||||
const knownVersions: Array<string> = [];
|
||||
|
||||
for (const directory of directories) {
|
||||
if (this.getIsDownloaded(directory)) {
|
||||
if (await this.getIsDownloaded(directory)) {
|
||||
knownVersions.push(directory);
|
||||
}
|
||||
}
|
||||
@@ -86,20 +164,63 @@ export class BinaryManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public getIsDownloaded(version: string) {
|
||||
const expectedPath = this.getElectronBinary(version);
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
private getDownloadPath(version: string) {
|
||||
return path.join(userData, 'electron-bin', version);
|
||||
public async 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unzip(zipPath: string, extractPath: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
/**
|
||||
* 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 actually 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;
|
||||
|
||||
process.noAsar = true;
|
||||
|
||||
extract(zipPath, { dir: extractPath }, (error) => {
|
||||
const options = {
|
||||
dir: extractPath,
|
||||
};
|
||||
|
||||
extract(zipPath, options, (error: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ElectronVersion } from '../interfaces';
|
||||
|
||||
export class Bisector {
|
||||
public revList: Array<ElectronVersion>;
|
||||
public minRev: number;
|
||||
public maxRev: number;
|
||||
private pivot: number;
|
||||
|
||||
constructor(revList: Array<ElectronVersion>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { remote } from 'electron';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { getTitle } from '../../utils/get-title';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ChromeMacProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
@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' onDoubleClick={this.handleDoubleClick}>
|
||||
<small>{getTitle(this.props.appState)}</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
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 { IpcEvents } from '../../ipc-events';
|
||||
import { idFromUrl, urlFromId } from '../../utils/gist';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface AddressBarProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
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.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) : '';
|
||||
|
||||
const { remoteLoader } = window.ElectronFiddle.app;
|
||||
|
||||
this.state = {
|
||||
value,
|
||||
loaders: {
|
||||
gist: remoteLoader.loadFiddleFromGist.bind(remoteLoader),
|
||||
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the form's submit event, trying to load whatever
|
||||
* URL was entered.
|
||||
*
|
||||
* @param {React.SyntheticEvent<HTMLFormElement>} event
|
||||
*/
|
||||
public handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
remoteLoader.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the component mounts, we'll subscribe to gistId changes
|
||||
*/
|
||||
public componentDidMount() {
|
||||
const { appState } = this.props;
|
||||
const { loaders } = this.state;
|
||||
reaction(
|
||||
() => 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the change event, which usually just updates the address bar's value
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.setState({ value: event.target.value });
|
||||
}
|
||||
|
||||
public renderLoadButton(isValueCorrect: boolean): JSX.Element {
|
||||
return (
|
||||
<Button
|
||||
disabled={!isValueCorrect}
|
||||
icon='cloud-download'
|
||||
text='Load Fiddle'
|
||||
onClick={this.submit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
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 });
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={this.handleSubmit}>
|
||||
<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,68 @@
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
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) {
|
||||
const { appState } = this.props;
|
||||
const response = appState.Bisector!.continue(isGood);
|
||||
if (Array.isArray(response)) {
|
||||
this.terminateBisect();
|
||||
const [minRev, maxRev] = response;
|
||||
appState.pushOutput(`[BISECT] Complete: Check between versions ${minRev.version} and ${maxRev.version}.`);
|
||||
} else {
|
||||
appState.setVersion(response.version);
|
||||
}
|
||||
}
|
||||
|
||||
public terminateBisect() {
|
||||
const { appState } = this.props;
|
||||
appState.Bisector = undefined;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
if (!!appState.Bisector) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon={'thumbs-up'}
|
||||
onClick={() => this.continueBisect(true)}
|
||||
/>
|
||||
<Button
|
||||
icon={'thumbs-down'}
|
||||
onClick={() => this.continueBisect(false)}
|
||||
/>
|
||||
<Button
|
||||
icon={'cross'}
|
||||
onClick={this.terminateBisect}
|
||||
>
|
||||
Cancel Bisect
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
icon='git-branch'
|
||||
text='Bisect'
|
||||
onClick={appState.toggleBisectDialog}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Button, Menu, 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { Button, ButtonGroup, Menu, MenuItem, Popover, Position } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { when } from 'mobx';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../shared-constants';
|
||||
import { getOctokit } from '../../utils/octokit';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface PublishButtonProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "publish" button takes care of logging you in.
|
||||
*
|
||||
* @export
|
||||
* @class PublishButton
|
||||
* @extends {React.Component<PublishButtonProps, PublishButtonState>}
|
||||
*/
|
||||
@observer
|
||||
export class PublishButton extends React.Component<PublishButtonProps> {
|
||||
constructor(props: PublishButtonProps) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.publishFiddle = this.publishFiddle.bind(this);
|
||||
this.setPrivate = this.setPrivate.bind(this);
|
||||
this.setPublic = this.setPublic.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user clicks the publish button, we either show the
|
||||
* authentication dialog or publish right away.
|
||||
*
|
||||
* If we're showing the authentication dialog, we wait for it
|
||||
* to be closed again (or a GitHub token to show up) before
|
||||
* we publish
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
* @memberof PublishButton
|
||||
*/
|
||||
public async handleClick(): Promise<void> {
|
||||
const { appState } = this.props;
|
||||
|
||||
if (!appState.gitHubToken) {
|
||||
appState.toggleAuthDialog();
|
||||
}
|
||||
|
||||
// Wait for the dialog to be closed again
|
||||
await when(() => !!appState.gitHubToken || !appState.isTokenDialogShowing);
|
||||
|
||||
if (appState.gitHubToken) {
|
||||
return this.publishFiddle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect with GitHub, publish the current Fiddle as a gist,
|
||||
* and update all related properties in the app state.
|
||||
*/
|
||||
public async publishFiddle(): Promise<void> {
|
||||
const { appState } = this.props;
|
||||
appState.isPublishing = true;
|
||||
|
||||
const octo = await getOctokit(this.props.appState);
|
||||
const { gitHubPublishAsPublic } = this.props.appState;
|
||||
const options = { includeDependencies: true, includeElectron: true };
|
||||
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
|
||||
|
||||
appState.gistId = gist.data.id;
|
||||
|
||||
console.log(`Publish Button: Publishing done`, { gist });
|
||||
} catch (error) {
|
||||
console.warn(`Could not publish gist`, { error });
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
appState.isPublishing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.props.appState;
|
||||
|
||||
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
|
||||
const privacyMenu = (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text='Private'
|
||||
icon='lock'
|
||||
active={!gitHubPublishAsPublic}
|
||||
onClick={this.setPrivate}
|
||||
/>
|
||||
<MenuItem
|
||||
text='Public'
|
||||
icon='unlock'
|
||||
active={gitHubPublishAsPublic}
|
||||
onClick={this.setPublic}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<fieldset disabled={isPublishing}>
|
||||
<ButtonGroup className='button-publish'>
|
||||
<Popover
|
||||
content={privacyMenu}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
icon={privacyIcon}
|
||||
/>
|
||||
</Popover>
|
||||
<Button
|
||||
onClick={this.handleClick}
|
||||
loading={isPublishing}
|
||||
icon='upload'
|
||||
text={isPublishing ? 'Publishing...' : 'Publish'}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
private setPrivacy(publishAsPublic: boolean) {
|
||||
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Button, IButtonProps, Spinner } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ElectronVersionState } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface RunnerState {
|
||||
}
|
||||
|
||||
export interface RunnerProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The runner component is responsible for actually launching the fiddle
|
||||
* with Electron. It also renders the button that does so.
|
||||
*
|
||||
* @class Runner
|
||||
* @extends {React.Component<RunnerProps, RunnerState>}
|
||||
*/
|
||||
@observer
|
||||
export class Runner extends React.Component<RunnerProps, RunnerState> {
|
||||
public render() {
|
||||
const { isRunning, currentElectronVersion } = this.props.appState;
|
||||
|
||||
const state = currentElectronVersion && currentElectronVersion.state;
|
||||
const props: IButtonProps = { className: 'button-run' };
|
||||
|
||||
if (state === ElectronVersionState.downloading) {
|
||||
props.text = 'Downloading';
|
||||
props.disabled = true;
|
||||
props.icon = <Spinner size={16} />;
|
||||
} else if (state === ElectronVersionState.ready) {
|
||||
if (isRunning) {
|
||||
props.active = true;
|
||||
props.text = 'Stop';
|
||||
props.onClick = window.ElectronFiddle.app.runner.stop;
|
||||
props.icon = 'stop';
|
||||
} else {
|
||||
props.text = 'Run';
|
||||
props.onClick = window.ElectronFiddle.app.runner.run;
|
||||
props.icon = 'play';
|
||||
}
|
||||
} else {
|
||||
props.text = 'Checking status';
|
||||
props.disabled = true;
|
||||
props.icon = <Spinner size={16} />;
|
||||
}
|
||||
|
||||
return <Button {...props} type={undefined} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { Button, ButtonGroup, MenuItem } from '@blueprintjs/core';
|
||||
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
|
||||
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}
|
||||
*/
|
||||
export function getItemLabel({ source, state, name }: ElectronVersion): string {
|
||||
let label = '';
|
||||
|
||||
if (source === ElectronVersionSource.local) {
|
||||
label = name || 'Local';
|
||||
} 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
|
||||
*/
|
||||
export 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
|
||||
*/
|
||||
export 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
|
||||
*/
|
||||
export 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;
|
||||
}
|
||||
|
||||
export interface VersionChooserProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
|
||||
export const getVersionsFromAppState = (appState: AppState) => {
|
||||
const { versions, versionsToShow, statesToShow } = appState;
|
||||
|
||||
return sortedElectronMap<ElectronVersion>(versions, (_key, item) => item)
|
||||
.filter((item) => {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we want to show the version
|
||||
if (!versionsToShow.includes(getReleaseChannel(item))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we want to show the state
|
||||
if (!statesToShow.includes(item.state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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 render() {
|
||||
const { currentElectronVersion, Bisector } = this.props.appState;
|
||||
const { version } = currentElectronVersion;
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ElectronVersionSelect
|
||||
filterable={true}
|
||||
items={getVersionsFromAppState(this.props.appState)}
|
||||
itemRenderer={renderItem}
|
||||
itemPredicate={filterItem}
|
||||
onItemSelect={this.onItemSelect}
|
||||
noResults={<MenuItem disabled={true} text='No results.' />}
|
||||
disabled={!!Bisector}
|
||||
>
|
||||
<Button
|
||||
className='version-chooser'
|
||||
text={`Electron v${version}`}
|
||||
icon={getItemIcon(currentElectronVersion)}
|
||||
disabled={!!Bisector}
|
||||
/>
|
||||
</ElectronVersionSelect>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,65 @@
|
||||
import * as React from 'react';
|
||||
import { Button, ControlGroup } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as Icon from '@fortawesome/react-fontawesome';
|
||||
import { faTerminal } from '@fortawesome/fontawesome-free-solid';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Runner } from './runner';
|
||||
import { VersionChooser } from './version-chooser';
|
||||
import { AppState } from '../app';
|
||||
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';
|
||||
|
||||
export interface CommandsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command bar, containing all the buttons doing
|
||||
* all the things
|
||||
*
|
||||
* @class Commands
|
||||
* @extends {React.Component<CommandsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class Commands extends React.Component<CommandsProps, {}> {
|
||||
constructor(props: CommandsProps) {
|
||||
super(props);
|
||||
|
||||
this.toggleConsole = this.toggleConsole.bind(this);
|
||||
}
|
||||
|
||||
public toggleConsole() {
|
||||
this.props.appState.isConsoleShowing = !this.props.appState.isConsoleShowing;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { isBisectCommandShowing: isBisectCommandShowing } = appState;
|
||||
|
||||
return (
|
||||
<div className='commands'>
|
||||
<div>
|
||||
<Runner appState={this.props.appState} />
|
||||
<VersionChooser appState={this.props.appState} />
|
||||
<ControlGroup fill={true} vertical={false}>
|
||||
<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}
|
||||
icon='console'
|
||||
text='Console'
|
||||
onClick={appState.toggleConsole}
|
||||
/>
|
||||
<EditorDropdown appState={appState} />
|
||||
</ControlGroup>
|
||||
</div>
|
||||
<div>
|
||||
<button className='button' onClick={this.toggleConsole}>
|
||||
<Icon icon={faTerminal} />
|
||||
</button>
|
||||
<AddressBar appState={appState} />
|
||||
<PublishButton appState={appState} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
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 { 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.setWarningDialogTexts({label: `Error: ${error}, please pick a different file.`});
|
||||
this.props.appState.isWarningDialogShowing = 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
|
||||
import { Button, Callout, Dialog, FileInput, InputGroup, Intent } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { NpmVersion } from '../../interfaces';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { getElectronNameForPlatform } from '../../utils/electron-name';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface AddVersionDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface AddVersionDialogState {
|
||||
isValidElectron: boolean;
|
||||
isValidVersion: boolean;
|
||||
folderPath?: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "add version" dialog allows users to add custom builds of Electron.
|
||||
*
|
||||
* @class AddVersionDialog
|
||||
* @extends {React.Component<AddVersionDialogProps, AddVersionDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class AddVersionDialog extends React.Component<AddVersionDialogProps, AddVersionDialogState> {
|
||||
constructor(props: AddVersionDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isValidVersion: false,
|
||||
isValidElectron: false,
|
||||
version: ''
|
||||
};
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClose = this.onClose.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change of the file input
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public async setFolderPath(folderPath: string) {
|
||||
const { binaryManager } = this.props.appState;
|
||||
const isValidElectron = !!await binaryManager.getIsDownloaded('custom', folderPath);
|
||||
|
||||
this.setState({ folderPath, isValidElectron });
|
||||
}
|
||||
|
||||
public onChangeVersion(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const version = event.target.value || '';
|
||||
const isValidVersion = !!semver.valid(version);
|
||||
|
||||
this.setState({
|
||||
version,
|
||||
isValidVersion,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of the dialog
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async onSubmit(): Promise<void> {
|
||||
const { folderPath, version } = this.state;
|
||||
|
||||
if (!folderPath) return;
|
||||
|
||||
const name = folderPath
|
||||
.slice(-20)
|
||||
.split(path.sep)
|
||||
.slice(1)
|
||||
.join(path.sep);
|
||||
|
||||
const toAdd: NpmVersion = {
|
||||
localPath: folderPath,
|
||||
version,
|
||||
name
|
||||
};
|
||||
|
||||
this.props.appState.addLocalVersion(toAdd);
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog
|
||||
*/
|
||||
public onClose() {
|
||||
this.props.appState.isAddVersionDialogShowing = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
const canSubmit = this.state.isValidElectron && this.state.isValidVersion;
|
||||
|
||||
return [
|
||||
(
|
||||
<Button
|
||||
icon='add'
|
||||
key='submit'
|
||||
disabled={!canSubmit}
|
||||
onClick={this.onSubmit}
|
||||
text='Add'
|
||||
/>
|
||||
), (
|
||||
<Button
|
||||
icon='cross'
|
||||
key='cancel'
|
||||
onClick={this.onClose}
|
||||
text='Cancel'
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isAddVersionDialogShowing } = this.props.appState;
|
||||
const inputProps = {
|
||||
onClick: (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
ipcRendererManager.send(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG);
|
||||
}
|
||||
};
|
||||
const { folderPath } = this.state;
|
||||
|
||||
const text = folderPath ||
|
||||
`Select the folder containing ${getElectronNameForPlatform()}...`;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isAddVersionDialogShowing}
|
||||
onClose={this.onClose}
|
||||
title='Add local Electron build'
|
||||
className='dialog-add-version'
|
||||
>
|
||||
<div className='bp3-dialog-body'>
|
||||
<FileInput
|
||||
id='custom-electron-version'
|
||||
inputProps={inputProps as any}
|
||||
text={text}
|
||||
/>
|
||||
<br />
|
||||
{this.renderPath()}
|
||||
</div>
|
||||
<div className='bp3-dialog-footer'>
|
||||
<div className='bp3-dialog-footer-actions'>
|
||||
{this.buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPath(): JSX.Element | null {
|
||||
const { folderPath, isValidElectron } = this.state;
|
||||
|
||||
if (!folderPath) return null;
|
||||
|
||||
const info = isValidElectron
|
||||
? `We found an ${getElectronNameForPlatform()} in this folder.`
|
||||
: `We did not find a ${getElectronNameForPlatform()} in this folder...`;
|
||||
|
||||
return (
|
||||
<Callout>
|
||||
{info}
|
||||
{this.renderVersionInput()}
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
private renderVersionInput(): JSX.Element | null {
|
||||
const { isValidElectron, isValidVersion, version } = this.state;
|
||||
|
||||
if (!isValidElectron) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
Please specify a version, used for typings and the name.
|
||||
Must be <code>semver</code> compliant.
|
||||
</p>
|
||||
<InputGroup
|
||||
intent={isValidVersion ? undefined : Intent.DANGER}
|
||||
value={version}
|
||||
onChange={this.onChangeVersion}
|
||||
placeholder='4.0.0'
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this component's state
|
||||
*/
|
||||
private reset(): void {
|
||||
this.setState({
|
||||
isValidElectron: false,
|
||||
isValidVersion: false,
|
||||
version: '',
|
||||
folderPath: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Button, Dialog, Label, MenuItem } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ElectronVersion } from '../../interfaces';
|
||||
import { Bisector } from '../bisect';
|
||||
import { AppState } from '../state';
|
||||
import { filterItem, getItemIcon, getVersionsFromAppState, renderItem } from './commands-version-chooser';
|
||||
|
||||
const ElectronVersionSelect = Select.ofType<ElectronVersion>();
|
||||
|
||||
export interface BisectDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface BisectDialogState {
|
||||
startIndex?: number;
|
||||
endIndex?: number;
|
||||
allVersions: Array<ElectronVersion>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "add version" dialog allows users to add custom builds of Electron.
|
||||
*
|
||||
* @class AddVersionDialog
|
||||
* @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);
|
||||
|
||||
const allVersions = getVersionsFromAppState(this.props.appState);
|
||||
this.state = { allVersions };
|
||||
}
|
||||
|
||||
public onBeginSelect(version: ElectronVersion) {
|
||||
this.setState({ startIndex: this.state.allVersions.indexOf(version) });
|
||||
}
|
||||
|
||||
public onEndSelect(version: ElectronVersion) {
|
||||
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;
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
const canSubmit =
|
||||
!!this.state.startIndex &&
|
||||
!!this.state.endIndex &&
|
||||
this.state.startIndex > this.state.endIndex;
|
||||
|
||||
return [
|
||||
(
|
||||
<Button
|
||||
icon='play'
|
||||
key='submit'
|
||||
disabled={!canSubmit}
|
||||
onClick={this.onSubmit}
|
||||
text='Begin'
|
||||
/>
|
||||
), (
|
||||
<Button
|
||||
icon='cross'
|
||||
key='cancel'
|
||||
onClick={this.onClose}
|
||||
text='Cancel'
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
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'>
|
||||
<Label>
|
||||
Earliest Version
|
||||
<ElectronVersionSelect
|
||||
filterable={true}
|
||||
items={allVersions}
|
||||
itemRenderer={renderItem}
|
||||
itemPredicate={filterItem}
|
||||
onItemSelect={this.onBeginSelect}
|
||||
noResults={<MenuItem disabled={true} text='No results.' />}
|
||||
>
|
||||
<Button
|
||||
text={startIndex ? `v${allVersions[startIndex].version}` : ``}
|
||||
icon={startIndex ? getItemIcon(allVersions[startIndex]) : 'small-minus'}
|
||||
fill={true}
|
||||
/>
|
||||
</ElectronVersionSelect>
|
||||
</Label>
|
||||
<Label>
|
||||
Latest Version
|
||||
<ElectronVersionSelect
|
||||
filterable={true}
|
||||
items={allVersions.slice(0, startIndex!)}
|
||||
itemRenderer={renderItem}
|
||||
itemPredicate={filterItem}
|
||||
onItemSelect={this.onEndSelect}
|
||||
noResults={<MenuItem disabled={true} text='No results.' />}
|
||||
disabled={!startIndex}
|
||||
>
|
||||
<Button
|
||||
text={endIndex ? `v${allVersions[endIndex].version}` : ``}
|
||||
icon={endIndex ? getItemIcon(allVersions[endIndex]) : 'small-minus'}
|
||||
fill={true}
|
||||
disabled={!startIndex}
|
||||
/>
|
||||
</ElectronVersionSelect>
|
||||
</Label>
|
||||
</div>
|
||||
<div className='bp3-dialog-footer'>
|
||||
<div className='bp3-dialog-footer-actions'>
|
||||
{this.buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ConfirmDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface ConfirmDialogState {
|
||||
}
|
||||
|
||||
/**
|
||||
* The token dialog prompts the user to either continue or cancel the operation.
|
||||
*
|
||||
* @export
|
||||
* @class ConfirmDialog
|
||||
* @extends {React.Component<ConfirmDialogProps, ConfirmDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class ConfirmDialog extends React.Component<ConfirmDialogProps, ConfirmDialogState> {
|
||||
constructor(props: ConfirmDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
public onClose(result: boolean) {
|
||||
this.props.appState.confirmationPromptLastResult = result;
|
||||
this.props.appState.toggleConfirmationPromptDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isConfirmationPromptShowing, confirmationDialogTexts } = this.props.appState;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
isOpen={isConfirmationPromptShowing}
|
||||
onClose={this.onClose}
|
||||
icon='help'
|
||||
confirmButtonText={confirmationDialogTexts.ok}
|
||||
cancelButtonText={confirmationDialogTexts.cancel}
|
||||
intent={Intent.PRIMARY}
|
||||
>
|
||||
<p>{confirmationDialogTexts.label}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
|
||||
import { Button, Callout, Dialog, InputGroup, Intent } from '@blueprintjs/core';
|
||||
import { clipboard, shell } from 'electron';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { getOctokit } from '../../utils/octokit';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface TokenDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface TokenDialogState {
|
||||
tokenInput: string;
|
||||
verifying: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
const TOKEN_SCOPES = [ 'gist' ].join();
|
||||
const TOKEN_DESCRIPTION = encodeURIComponent('Fiddle Gist Token');
|
||||
const GENERATE_TOKEN_URL = `https://github.com/settings/tokens/new?scopes=${TOKEN_SCOPES}&description=${TOKEN_DESCRIPTION}`;
|
||||
|
||||
/**
|
||||
* 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 TokenDialog
|
||||
* @extends {React.Component<TokenDialogProps, TokenDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogState> {
|
||||
constructor(props: TokenDialogProps) {
|
||||
super(props);
|
||||
|
||||
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);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of a token
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
* @memberof TokenDialog
|
||||
*/
|
||||
public async onSubmitToken(): Promise<void> {
|
||||
if (!this.state.tokenInput) return;
|
||||
this.setState({ verifying: true });
|
||||
this.props.appState.gitHubToken = this.state.tokenInput;
|
||||
|
||||
const octo = await getOctokit(this.props.appState);
|
||||
|
||||
try {
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.appState.isTokenDialogShowing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog
|
||||
*/
|
||||
public onClose() {
|
||||
this.props.appState.isTokenDialogShowing = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this component's state
|
||||
*/
|
||||
public reset(): void {
|
||||
this.setState({
|
||||
verifying: false,
|
||||
error: false,
|
||||
tokenInput: ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens GitHub's page for token generation
|
||||
*
|
||||
* @memberof TokenDialog
|
||||
*/
|
||||
public openGenerateTokenExternal() {
|
||||
shell.openExternal(GENERATE_TOKEN_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the input field receives focus, we check the clipboard.
|
||||
* Maybe there's already something token-like there!
|
||||
*
|
||||
* @returns
|
||||
* @memberof TokenDialog
|
||||
*/
|
||||
public onTokenInputFocused() {
|
||||
const text = (clipboard.readText() || '').trim();
|
||||
|
||||
if (text.length !== 40) return;
|
||||
if (!/^[a-z0-9]+$/.test(text)) return;
|
||||
|
||||
this.setState({ tokenInput: text });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the change event, which usually just updates the address bar's value
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
* @memberof AddressBar
|
||||
*/
|
||||
public handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.setState({ tokenInput: event.target.value });
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
const canSubmit = !!this.state.tokenInput;
|
||||
|
||||
return [
|
||||
(
|
||||
<Button
|
||||
key='done'
|
||||
disabled={!canSubmit}
|
||||
onClick={this.onSubmitToken}
|
||||
loading={this.state.verifying}
|
||||
text='Done'
|
||||
icon='log-in'
|
||||
/>
|
||||
), (
|
||||
<Button
|
||||
key='cancel'
|
||||
text='Cancel'
|
||||
icon='log-out'
|
||||
onClick={this.onClose}
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
get invalidWarning() {
|
||||
return (
|
||||
<>
|
||||
<Callout intent={Intent.DANGER}>
|
||||
Please provide a valid GitHub Personal Access Token
|
||||
</Callout>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isTokenDialogShowing } = this.props.appState;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isTokenDialogShowing}
|
||||
onClose={this.onClose}
|
||||
title='GitHub Token'
|
||||
>
|
||||
<div className='bp3-dialog-body'>
|
||||
<p>
|
||||
Generate a <a onClick={this.openGenerateTokenExternal}>GitHub Personal Access Token</a> and paste it here:
|
||||
</p>
|
||||
|
||||
{this.state.error ? this.invalidWarning : null}
|
||||
|
||||
<InputGroup
|
||||
value={this.state.tokenInput || ''}
|
||||
onFocus={this.onTokenInputFocused}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='bp3-dialog-footer'>
|
||||
<div className='bp3-dialog-footer-actions'>
|
||||
{this.buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
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 prompts the user to either continue or cancel the operation.
|
||||
*
|
||||
* @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.toggleWarningDialog();
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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 { ConfirmDialog } from './dialog-confirm';
|
||||
import { TokenDialog } from './dialog-token';
|
||||
import { WarningDialog } from './dialog-warning';
|
||||
import { Settings } from './settings';
|
||||
|
||||
export interface DialogsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialogs (like the GitHub PAT input).
|
||||
*
|
||||
* @class Dialogs
|
||||
* @extends {React.Component<DialogsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class Dialogs extends React.Component<DialogsProps, {}> {
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const {
|
||||
isTokenDialogShowing,
|
||||
isSettingsShowing,
|
||||
isAddVersionDialogShowing,
|
||||
isThemeDialogShowing,
|
||||
isBisectDialogShowing
|
||||
} = appState;
|
||||
const maybeToken = isTokenDialogShowing
|
||||
? <TokenDialog key='dialogs' appState={appState} />
|
||||
: null;
|
||||
const maybeSettings = isSettingsShowing
|
||||
? <Settings key='settings' appState={appState} />
|
||||
: null;
|
||||
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 eitherWarningOrPrompt = appState.isWarningDialogShowing
|
||||
? <WarningDialog appState={appState} />
|
||||
: <ConfirmDialog appState={appState} />;
|
||||
|
||||
return (
|
||||
<div key='dialogs' className='dialogs'>
|
||||
{maybeToken}
|
||||
{maybeSettings}
|
||||
{maybeAddLocalVersion}
|
||||
{maybeMonaco}
|
||||
{maybeBisect}
|
||||
{eitherWarningOrPrompt}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export class EditorTitle extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<div className='editorTitle'>
|
||||
<span>Main Process</span>
|
||||
<span>Renderer Process</span>
|
||||
<span>HTML</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Credit goes in large part to https://github.com/superRaytin/react-monaco-editor,
|
||||
// this component is a changed version of it.
|
||||
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as React from 'react';
|
||||
|
||||
import { EditorId } from '../../interfaces';
|
||||
import { getContent } from '../content';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface EditorProps {
|
||||
appState: AppState;
|
||||
monaco: typeof MonacoType;
|
||||
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;
|
||||
}
|
||||
|
||||
export class Editor extends React.Component<EditorProps> {
|
||||
public editor: MonacoType.editor.IStandaloneCodeEditor;
|
||||
public language: string = 'javascript';
|
||||
public value: string = '';
|
||||
|
||||
private containerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: EditorProps) {
|
||||
super(props);
|
||||
|
||||
this.language = props.id === 'html' ? 'html' : 'javascript';
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.initMonaco();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.destroyMonaco();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the editor having been mounted. This refers to Monaco's
|
||||
* mount, not React's.
|
||||
*
|
||||
* @param {MonacoType.editor.IStandaloneCodeEditor} editor
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Monaco.
|
||||
*/
|
||||
public async initMonaco() {
|
||||
const { monaco, monacoOptions: monacoOptions } = this.props;
|
||||
const ref = this.containerRef.current;
|
||||
|
||||
if (ref) {
|
||||
this.editor = monaco.editor.create(ref, {
|
||||
language: this.language,
|
||||
theme: 'main',
|
||||
contextmenu: false,
|
||||
model: null,
|
||||
...monacoOptions
|
||||
});
|
||||
|
||||
await this.editorDidMount(this.editor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy Monaco.
|
||||
*/
|
||||
public destroyMonaco() {
|
||||
if (typeof this.editor !== 'undefined') {
|
||||
console.log('Editor: Disposing');
|
||||
this.editor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div className='editorContainer' ref={this.containerRef} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content on the editor, including the model and the view state.
|
||||
*
|
||||
* @private
|
||||
* @memberof Editor
|
||||
*/
|
||||
private async setContent() {
|
||||
const { appState, id, monaco } = this.props;
|
||||
const { version } = appState;
|
||||
|
||||
const backup = appState.getAndRemoveEditorValueBackup(id);
|
||||
|
||||
if (backup && backup.model) {
|
||||
console.log(`Editor: Backup found, restoring state`);
|
||||
|
||||
if (backup.viewState) {
|
||||
this.editor.restoreViewState(backup.viewState);
|
||||
}
|
||||
|
||||
this.editor.setModel(backup.model);
|
||||
} else {
|
||||
const value = await getContent(id, version);
|
||||
const model = monaco.editor.createModel(value, this.language);
|
||||
model.updateOptions({
|
||||
tabSize: 2
|
||||
});
|
||||
|
||||
this.editor.setModel(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { reaction } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as React from 'react';
|
||||
import { Mosaic, MosaicBranch, MosaicNode, MosaicWindow, MosaicWindowProps } from 'react-mosaic-component';
|
||||
|
||||
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 { 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';
|
||||
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: {
|
||||
enabled: false
|
||||
},
|
||||
wordWrap: 'on'
|
||||
};
|
||||
|
||||
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)',
|
||||
docsDemo: 'Docs & Demos',
|
||||
};
|
||||
|
||||
export interface EditorsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface EditorsState {
|
||||
monaco?: typeof MonacoType;
|
||||
isMounted?: boolean;
|
||||
monacoOptions: MonacoType.editor.IEditorOptions;
|
||||
}
|
||||
|
||||
@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.state = { monacoOptions: defaultMonacoOptions };
|
||||
|
||||
this.loadMonaco();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed right after the component mounts. We'll setup the IPC listeners here.
|
||||
*
|
||||
* @memberof Editors
|
||||
*/
|
||||
public componentDidMount() {
|
||||
ipcRendererManager.on(IpcEvents.MONACO_EXECUTE_COMMAND, (_event, cmd: string) => {
|
||||
this.executeCommand(cmd);
|
||||
});
|
||||
|
||||
ipcRendererManager.on(IpcEvents.FS_NEW_FIDDLE, async (_event) => {
|
||||
const { version } = this.props.appState;
|
||||
|
||||
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.MONACO_TOGGLE_OPTION, (_event, cmd: string) => {
|
||||
this.toggleEditorOption(cmd);
|
||||
});
|
||||
|
||||
this.setState({ isMounted: true });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.disposeLayoutAutorun();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to execute a given commandId on the currently focused editor
|
||||
*
|
||||
* @param {string} commandId
|
||||
* @memberof Editors
|
||||
*/
|
||||
public executeCommand(commandId: string) {
|
||||
const editor = getFocusedEditor();
|
||||
|
||||
if (editor) {
|
||||
const command = editor.getAction(commandId);
|
||||
|
||||
console.log(`Editors: Trying to run ${command.id}. Supported: ${command.isSupported}`);
|
||||
|
||||
if (command && command.isSupported()) {
|
||||
command.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public toggleEditorOption(path: string): boolean {
|
||||
if (!window.ElectronFiddle.editors) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monacoOptions } = this.state;
|
||||
const newOptions = { ...monacoOptions };
|
||||
const currentSetting = getAtPath(path, newOptions);
|
||||
|
||||
setAtPath(path, newOptions, toggleMonaco(currentSetting));
|
||||
|
||||
Object.keys(window.ElectronFiddle.editors)
|
||||
.forEach((key) => {
|
||||
const editor: MonacoType.editor.IStandaloneCodeEditor | null
|
||||
= window.ElectronFiddle.editors[key];
|
||||
|
||||
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 docsDemoGoHomeMaybe = id === PanelId.docsDemo
|
||||
? <DocsDemoGoHomeButton id={id} appState={this.props.appState} />
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Left */}
|
||||
<div>
|
||||
<h5>
|
||||
{title}
|
||||
</h5>
|
||||
</div>
|
||||
{/* Middle */}
|
||||
<div />
|
||||
{/* Right */}
|
||||
<div className='mosaic-controls'>
|
||||
{docsDemoGoHomeMaybe}
|
||||
<MaximizeButton id={id} appState={this.props.appState} />
|
||||
<RemoveButton id={id} appState={this.props.appState} />
|
||||
</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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { monaco } = this.state;
|
||||
|
||||
if (!monaco) return null;
|
||||
|
||||
return (
|
||||
<Mosaic<EditorId | PanelId>
|
||||
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.
|
||||
*/
|
||||
public async loadMonaco(): Promise<void> {
|
||||
const { app } = window.ElectronFiddle;
|
||||
const loader = require('monaco-loader');
|
||||
const monaco = app.monaco || await loader();
|
||||
|
||||
if (!app.monaco) {
|
||||
app.monaco = monaco;
|
||||
}
|
||||
|
||||
if (!this.state || !this.state.isMounted) {
|
||||
this.state = {
|
||||
monaco,
|
||||
monacoOptions: defaultMonacoOptions
|
||||
};
|
||||
} else {
|
||||
this.setState({ monaco });
|
||||
}
|
||||
|
||||
activateTheme(monaco, undefined, this.props.appState.theme);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { EditorTitle } from './editor-title';
|
||||
import { AppState } from '../state';
|
||||
import { ChromeMac } from './chrome-mac';
|
||||
import { Commands } from './commands';
|
||||
import { AppState } from '../app';
|
||||
import { Output } from './output';
|
||||
import { WelcomeTour } from './tour-welcome';
|
||||
|
||||
export interface HeaderProps {
|
||||
appState: AppState;
|
||||
}
|
||||
@observer
|
||||
|
||||
/**
|
||||
* Everything above the editors, so buttons and the address bar.
|
||||
*
|
||||
* @class Header
|
||||
* @extends {React.Component<HeaderProps, HeaderState>}
|
||||
*/
|
||||
export class Header extends React.Component<HeaderProps, {}> {
|
||||
public render() {
|
||||
return [
|
||||
<Commands key='commands' appState={this.props.appState} />,
|
||||
<Output key='output' appState={this.props.appState} />,
|
||||
<EditorTitle key='titles' />
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<ChromeMac appState={this.props.appState} />
|
||||
<header id='header'>
|
||||
<Commands key='commands' 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,38 +1,113 @@
|
||||
import * as React from 'react';
|
||||
import { autorun } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import { MosaicContext } from 'react-mosaic-component';
|
||||
|
||||
import { AppState } from '../app';
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component represents the "console" that is shown
|
||||
* whenever a Fiddle is launched in Electron.
|
||||
*
|
||||
* @class Output
|
||||
* @extends {React.Component<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) {
|
||||
super(props);
|
||||
|
||||
this.renderTimestamp = this.renderTimestamp.bind(this);
|
||||
this.renderEntry = this.renderEntry.bind(this);
|
||||
}
|
||||
|
||||
public renderEntry(entry: OutputEntry) {
|
||||
const ts = new Date(entry.timestamp).toLocaleTimeString();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the timestamp
|
||||
*
|
||||
* @param {number} ts
|
||||
* @returns {string}
|
||||
*/
|
||||
public renderTimestamp(ts: number): string {
|
||||
const { renderTimestamp } = this.props;
|
||||
|
||||
if (renderTimestamp) {
|
||||
return renderTimestamp(ts);
|
||||
} else {
|
||||
return new Date(ts).toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An individual entry might span multiple lines. To ensure that
|
||||
* each line has a timestamp, this method might split up entries.
|
||||
*
|
||||
* @param {OutputEntry} entry
|
||||
* @returns {Array<JSX.Element>}
|
||||
* @memberof Output
|
||||
*/
|
||||
public renderEntry(entry: OutputEntry, index: number): Array<JSX.Element> {
|
||||
const ts = this.renderTimestamp(entry.timestamp);
|
||||
const timestamp = <span className='timestamp'>{ts}</span>;
|
||||
const lines = entry.text.split(/\r?\n/);
|
||||
const style: React.CSSProperties = entry.isNotPre ? { whiteSpace: 'initial' } : {};
|
||||
|
||||
|
||||
return lines.map((text) => (
|
||||
<p>{timestamp}{text}</p>
|
||||
return lines.map((text, lineIndex) => (
|
||||
<p style={style} key={`${entry.timestamp}--${index}--${lineIndex}`}>
|
||||
{timestamp}{text}
|
||||
</p>
|
||||
));
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isConsoleShowing } = this.props.appState;
|
||||
const className = isConsoleShowing ? 'output showing' : 'output';
|
||||
public render(): JSX.Element | null {
|
||||
const { output, isConsoleShowing } = this.props.appState;
|
||||
|
||||
const lines = this.props.appState.output.map(this.renderEntry);
|
||||
if (!isConsoleShowing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The last 1000 lines
|
||||
const lines = output
|
||||
.slice(Math.max(output.length - 1000, 1))
|
||||
.map(this.renderEntry);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='output' ref={this.outputRef}>
|
||||
{lines}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { observer } from 'mobx-react';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
|
||||
import { normalizeVersion } from '../../utils/normalize-version';
|
||||
import { AppState } from '../app';
|
||||
import { findModules, installModules } from '../npm';
|
||||
|
||||
export interface RunnerState {
|
||||
isRunning: boolean;
|
||||
}
|
||||
|
||||
export interface RunnerProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
@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.pushData = this.pushData.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
this.state = {
|
||||
isRunning: false
|
||||
};
|
||||
|
||||
this.pushData('Console ready 🔬');
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { versions, version } = this.props.appState;
|
||||
const { isRunning } = this.state;
|
||||
if (!versions || !version) return null;
|
||||
|
||||
const state = versions[normalizeVersion(version)].state;
|
||||
|
||||
let text = 'Run';
|
||||
let action = this.run;
|
||||
|
||||
if (state === 'downloading') {
|
||||
text = 'Downloading';
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
text = 'Stop';
|
||||
action = this.stop;
|
||||
}
|
||||
|
||||
return <button className='button' onClick={() => action()}>{text}</button>;
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
if (this.child) {
|
||||
this.child.kill();
|
||||
this.setState({
|
||||
isRunning: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public pushData(data: string | Buffer) {
|
||||
const strData = data.toString();
|
||||
if (strData.startsWith('Debugger listening on ws://')) return;
|
||||
|
||||
this.props.appState.output.push({
|
||||
timestamp: Date.now(),
|
||||
text: strData
|
||||
});
|
||||
}
|
||||
|
||||
public async installModules(values: any, dir: string) {
|
||||
const files = [ values.main, values.renderer ];
|
||||
const modules: Array<string> = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
modules.push(...findModules(file));
|
||||
});
|
||||
|
||||
if (modules.length === 0) return;
|
||||
|
||||
this.pushData(`Installing npm modules: ${modules.join(', ')}...`);
|
||||
this.pushData(await installModules({ dir }, ...modules));
|
||||
}
|
||||
|
||||
public async run() {
|
||||
const values = (window as any).electronFiddle.getValues();
|
||||
const tmpdir = (tmp as any).dirSync();
|
||||
const { binaryManager, version } = this.props.appState;
|
||||
|
||||
this.props.appState.isConsoleShowing = true;
|
||||
|
||||
try {
|
||||
await fs.writeFile(path.join(tmpdir.name, 'index.html'), values.html);
|
||||
await fs.writeFile(path.join(tmpdir.name, 'main.js'), values.main);
|
||||
await fs.writeFile(path.join(tmpdir.name, 'renderer.js'), values.renderer);
|
||||
await fs.writeFile(path.join(tmpdir.name, 'package.json'), values.package);
|
||||
await this.installModules(values, tmpdir.name);
|
||||
} catch (error) {
|
||||
console.error('Could not write files', error);
|
||||
}
|
||||
|
||||
if (!binaryManager.getIsDownloaded(version)) {
|
||||
console.warn(`Binary ${version} not ready`);
|
||||
return;
|
||||
}
|
||||
|
||||
const binaryPath = this.props.appState.binaryManager.getElectronBinary(version);
|
||||
console.log(`Binary ${binaryPath} ready, launching`);
|
||||
|
||||
this.child = spawn(binaryPath, [ tmpdir.name, '--inspect' ]);
|
||||
this.setState({ isRunning: true });
|
||||
this.pushData('Electron started.');
|
||||
this.child.stdout.on('data', this.pushData);
|
||||
this.child.stderr.on('data', this.pushData);
|
||||
this.child.on('close', (code) => {
|
||||
this.pushData(`Electron exited with code ${code.toString()}.`);
|
||||
this.setState({ isRunning: false });
|
||||
tmpdir.removeCallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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, CreditsSettingsState>}
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
public renderContributors(): Array<JSX.Element> {
|
||||
const { contributors } = this.state;
|
||||
|
||||
return contributors.map(({ name, avatar, url, login, location, bio }) => {
|
||||
const maybeLocation = location
|
||||
? <p className='location'>📍 {location}</p>
|
||||
: null;
|
||||
const maybeBio = bio
|
||||
? <small className='bio'>{bio}</small>
|
||||
: null;
|
||||
const style: React.CSSProperties = {
|
||||
backgroundImage: `url(${avatar})`
|
||||
};
|
||||
const onClick = () => shell.openExternal(url);
|
||||
|
||||
return (
|
||||
<Card interactive={true} key={login} className='contributor' onClick={onClick}>
|
||||
<div className='avatar' style={style} />
|
||||
<div className='details'>
|
||||
<h5 className='name'>{name || login}</h5>
|
||||
{maybeLocation}
|
||||
{maybeBio}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Credits</h2>
|
||||
<Callout>
|
||||
Electron Fiddle is, just like Electron, a free open source project
|
||||
welcoming contributors of all genders, cultures, and backgrounds. We
|
||||
would like to thank those who helped to make Electron Fiddle:
|
||||
</Callout>
|
||||
<br />
|
||||
<div className='contributors'>
|
||||
{this.renderContributors()}
|
||||
</div>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Callout,
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
HTMLTable,
|
||||
IButtonProps,
|
||||
Icon,
|
||||
IconName
|
||||
} from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
|
||||
import { normalizeVersion } from '../../utils/normalize-version';
|
||||
import { sortedElectronMap } from '../../utils/sorted-electron-map';
|
||||
import { AppState } from '../state';
|
||||
import { ElectronReleaseChannel, getReleaseChannel } from '../versions';
|
||||
|
||||
export interface ElectronSettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface ElectronSettingsState {
|
||||
isDownloadingAll: boolean;
|
||||
isDeletingAll: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage Electron-related preferences.
|
||||
*
|
||||
* @class ElectronSettings
|
||||
* @extends {React.Component<ElectronSettingsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class ElectronSettings extends React.Component<ElectronSettingsProps, ElectronSettingsState> {
|
||||
constructor(props: ElectronSettingsProps) {
|
||||
super(props);
|
||||
|
||||
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);
|
||||
|
||||
this.state = {
|
||||
isDownloadingAll: false,
|
||||
isDeletingAll: false
|
||||
};
|
||||
}
|
||||
|
||||
public handleDownloadClick() {
|
||||
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 ElectronVersionState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in which channels should be displayed.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleChannelChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { id, checked } = event.currentTarget;
|
||||
const { appState } = this.props;
|
||||
|
||||
if (!checked) {
|
||||
appState.versionsToShow = appState.versionsToShow.filter((c) => c !== id);
|
||||
} else {
|
||||
appState.versionsToShow.push(id as ElectronReleaseChannel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download all versions of Electron.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async handleDownloadAll(): Promise<void> {
|
||||
this.setState({ isDownloadingAll: true });
|
||||
|
||||
const { versions, downloadVersion } = this.props.appState;
|
||||
|
||||
for (const key of Object.keys(versions)) {
|
||||
await downloadVersion(key);
|
||||
}
|
||||
|
||||
this.setState({ isDownloadingAll: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all downloaded versions of Electron.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async handleDeleteAll(): Promise<void> {
|
||||
this.setState({ isDeletingAll: true });
|
||||
|
||||
const { versions, removeVersion, version } = this.props.appState;
|
||||
|
||||
for (const key in versions) {
|
||||
// If this isn't the currently selected version, remove it
|
||||
if (normalizeVersion(key) !== normalizeVersion(version)) {
|
||||
await removeVersion(key);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ isDeletingAll: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the "add local version" dialog
|
||||
*/
|
||||
public handleAddVersion(): void {
|
||||
this.props.appState.toggleAddVersionDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className='settings-electron'>
|
||||
<h2>Electron Settings</h2>
|
||||
<Callout>
|
||||
{this.renderVersionChannelOptions()}
|
||||
{this.renderVersionStateOptions()}
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
{this.renderAdvancedButtons()}
|
||||
{this.renderTable()}
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the various buttons for advanced operations
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderAdvancedButtons(): JSX.Element {
|
||||
const { isDownloadingAll, isDeletingAll } = this.state;
|
||||
const { isUpdatingElectronVersions } = this.props.appState;
|
||||
const isWorking = isDownloadingAll || isDeletingAll;
|
||||
|
||||
return (
|
||||
<ButtonGroup fill={true}>
|
||||
<Button
|
||||
disabled={isUpdatingElectronVersions}
|
||||
onClick={this.handleDownloadClick}
|
||||
loading={isUpdatingElectronVersions}
|
||||
icon='numbered-list'
|
||||
text='Update Electron Release List'
|
||||
/>
|
||||
<Button
|
||||
disabled={isWorking}
|
||||
icon='download'
|
||||
onClick={this.handleDownloadAll}
|
||||
text='Download All Versions'
|
||||
/>
|
||||
<Button
|
||||
disabled={isWorking}
|
||||
icon='trash'
|
||||
onClick={this.handleDeleteAll}
|
||||
text='Delete All Downloads'
|
||||
/>
|
||||
<Button
|
||||
icon='document-open'
|
||||
onClick={this.handleAddVersion}
|
||||
text='Add Local Electron Build'
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the various options for which versions should be displayed
|
||||
* in the small dropdown.
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderVersionStateOptions(): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const getIsChecked = (state: ElectronVersionState) => {
|
||||
return appState.statesToShow.includes(state);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label='Include Electron versions that are:'
|
||||
>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronVersionState.downloading)}
|
||||
label='Downloading'
|
||||
id='downloading'
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronVersionState.ready)}
|
||||
label='Downloaded'
|
||||
id='ready'
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronVersionState.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.versionsToShow.includes(channel);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label='Include Electron versions from these release channels:'
|
||||
>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.stable)}
|
||||
label='Stable'
|
||||
id='Stable'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the table with Electron versions.
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderTable(): JSX.Element {
|
||||
return (
|
||||
<HTMLTable
|
||||
className='electron-versions-table'
|
||||
striped={true}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Status</th>
|
||||
<th className='action'>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderTableRows()}
|
||||
</tbody>
|
||||
</HTMLTable>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the rows with Electron version, returning an Arary.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
private renderTableRows(): Array<JSX.Element | null> {
|
||||
const { versions, versionsToShow, 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))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we want to show the state
|
||||
if (!statesToShow.includes(item.state)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={item.version}>
|
||||
<td>{item.version}</td>
|
||||
<td>{this.renderHumanState(item)}</td>
|
||||
<td className='action'>{this.renderAction(key, item)}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable state indicator for an Electron version
|
||||
*
|
||||
* @param {ElectronVersion} item
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderHumanState(item: ElectronVersion): JSX.Element {
|
||||
const { state } = item;
|
||||
let icon: IconName = 'box';
|
||||
let humanState = 'Downloaded';
|
||||
|
||||
if (state === ElectronVersionState.downloading) {
|
||||
icon = 'cloud-download';
|
||||
humanState = 'Downloading';
|
||||
} else if (state === ElectronVersionState.unknown) {
|
||||
icon = 'cloud';
|
||||
humanState = 'Not downloaded';
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon={icon} /> {humanState}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the action for a single Electron version
|
||||
*
|
||||
* @private
|
||||
* @param {string} key
|
||||
* @param {ElectronVersion} item
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderAction(key: string, item: ElectronVersion): JSX.Element {
|
||||
const { state, source } = item;
|
||||
const { appState } = this.props;
|
||||
const buttonProps: IButtonProps = {
|
||||
fill: true,
|
||||
small: true
|
||||
};
|
||||
|
||||
// Already downloaded
|
||||
if (state === 'ready') {
|
||||
buttonProps.onClick = () => appState.removeVersion(key);
|
||||
buttonProps.icon = 'trash';
|
||||
buttonProps.text = source === ElectronVersionSource.local
|
||||
? 'Remove'
|
||||
: 'Delete';
|
||||
} else if (state === 'downloading') {
|
||||
buttonProps.disabled = true;
|
||||
buttonProps.loading = true;
|
||||
buttonProps.text = 'Downloading';
|
||||
buttonProps.icon = 'cloud-download';
|
||||
} else {
|
||||
buttonProps.disabled = false;
|
||||
buttonProps.loading = false;
|
||||
buttonProps.text = 'Download';
|
||||
buttonProps.icon = 'cloud-download';
|
||||
buttonProps.onClick = () => appState.downloadVersion(key);
|
||||
}
|
||||
|
||||
return <Button {...buttonProps} type={undefined} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Callout, Checkbox, FormGroup } 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isKeepingUserDataDirs, isEnablingElectronLogging } = 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { Button, Callout, FormGroup, MenuItem } from '@blueprintjs/core';
|
||||
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
|
||||
import { shell } from 'electron';
|
||||
import * as fsType from 'fs-extra';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
|
||||
import { highlightText } from '../../utils/highlight-text';
|
||||
import { fancyImport } from '../../utils/import';
|
||||
import { AppState } from '../state';
|
||||
import { getAvailableThemes, getTheme, THEMES_PATH } from '../themes';
|
||||
import { LoadedFiddleTheme } from '../themes-defaults';
|
||||
|
||||
const ThemeSelect = Select.ofType<LoadedFiddleTheme>();
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> predicate for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {string} query
|
||||
* @param {ElectronVersion} { version }
|
||||
* @returns
|
||||
*/
|
||||
export const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
|
||||
return name.toLowerCase().includes(query.toLowerCase());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> <MenuItem /> for Electron
|
||||
* versions.
|
||||
*
|
||||
* @param {ElectronVersion} item
|
||||
* @param {IItemRendererProps} { handleClick, modifiers, query }
|
||||
* @returns
|
||||
*/
|
||||
export const renderItem: ItemRenderer<LoadedFiddleTheme> = (item, { handleClick, modifiers, query }) => {
|
||||
if (!modifiers.matchesPredicate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
text={highlightText(item.name, query)}
|
||||
key={item.name}
|
||||
onClick={handleClick}
|
||||
icon='media'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface AppearanceSettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface AppearanceSettingsState {
|
||||
themes: Array<LoadedFiddleTheme>;
|
||||
selectedTheme?: LoadedFiddleTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage GitHub-related preferences.
|
||||
*
|
||||
* @class GitHubSettings
|
||||
* @extends {React.Component<AppearanceSettingsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class AppearanceSettings extends React.Component<
|
||||
AppearanceSettingsProps, AppearanceSettingsState
|
||||
> {
|
||||
public constructor(props: AppearanceSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.openThemeFolder = this.openThemeFolder.bind(this);
|
||||
this.handleAddTheme = this.handleAddTheme.bind(this);
|
||||
|
||||
this.state = {
|
||||
themes: []
|
||||
};
|
||||
|
||||
getAvailableThemes().then((themes) => {
|
||||
const { theme } = this.props.appState;
|
||||
const selectedTheme = theme &&
|
||||
themes.find(({ file }) => file === theme) || themes[0];
|
||||
|
||||
this.setState({ themes, selectedTheme });
|
||||
});
|
||||
|
||||
this.createNewThemeFromCurrent = this.createNewThemeFromCurrent.bind(this);
|
||||
this.openThemeFolder = this.openThemeFolder.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle change, which usually means that we'd like update
|
||||
* the current theme.
|
||||
*
|
||||
* @param {LoadedFiddleTheme} theme
|
||||
*/
|
||||
public handleChange(theme: LoadedFiddleTheme) {
|
||||
this.setState({ selectedTheme: theme });
|
||||
this.props.appState.setTheme(theme.file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new theme from the current template.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* @memberof AppearanceSettings
|
||||
*/
|
||||
public async createNewThemeFromCurrent(): Promise<boolean> {
|
||||
const { appState } = this.props;
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
const theme = await getTheme(appState.theme);
|
||||
|
||||
try {
|
||||
const namor = await fancyImport<any>('namor');
|
||||
const name = namor.generate({ words: 2, numbers: 0 });
|
||||
const themePath = path.join(THEMES_PATH, `${name}.json`);
|
||||
|
||||
await fs.outputJSON(themePath, {
|
||||
...theme,
|
||||
name,
|
||||
file: undefined,
|
||||
css: undefined
|
||||
}, { spaces: 2 });
|
||||
|
||||
shell.showItemInFolder(themePath);
|
||||
|
||||
this.setState({themes: await getAvailableThemes()});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Themes: Failed to create new theme from current`, error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the themes folder in .electron-fiddle if one does not
|
||||
* exist yet, then shows that folder in the Finder/Explorer.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public async openThemeFolder(): Promise<boolean> {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
|
||||
try {
|
||||
await fs.ensureDir(THEMES_PATH);
|
||||
await shell.showItemInFolder(THEMES_PATH);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Appearance Settings: Could not open themes folder`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { selectedTheme } = this.state;
|
||||
const selectedName = selectedTheme && selectedTheme.name || 'Select a theme';
|
||||
|
||||
return (
|
||||
<div className='settings-appearance'>
|
||||
<h4>Appearance</h4>
|
||||
<FormGroup
|
||||
label='Choose your theme'
|
||||
inline={true}
|
||||
>
|
||||
<ThemeSelect
|
||||
filterable={true}
|
||||
items={this.state.themes}
|
||||
itemRenderer={renderItem}
|
||||
itemPredicate={filterItem}
|
||||
onItemSelect={this.handleChange}
|
||||
noResults={<MenuItem disabled={true} text='No results.' />}
|
||||
>
|
||||
<Button
|
||||
text={selectedName}
|
||||
icon='tint'
|
||||
/>
|
||||
</ThemeSelect>
|
||||
</FormGroup>
|
||||
<Callout>
|
||||
<p>
|
||||
To add themes, add JSON theme files to <a
|
||||
id='open-theme-folder'
|
||||
onClick={this.openThemeFolder}
|
||||
>
|
||||
<code>{THEMES_PATH}</code>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Button, Callout } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface GitHubSettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage GitHub-related preferences.
|
||||
*
|
||||
* @class GitHubSettings
|
||||
* @extends {React.Component<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 {
|
||||
return (
|
||||
<Callout>
|
||||
<p>
|
||||
Your fiddles can be published as GitHub Gists -
|
||||
that way you can share your fiddles with the world!
|
||||
</p>
|
||||
<Button onClick={this.signIn} icon='log-in' text='Sign in'/>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "logged in" settings experience.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
public renderSignedIn(): JSX.Element {
|
||||
const { gitHubLogin } = this.props.appState;
|
||||
const signOut = this.props.appState.signOutGitHub;
|
||||
|
||||
return (
|
||||
<Callout>
|
||||
<p>
|
||||
Your fiddles can be published as public GitHub Gists.
|
||||
Using the personal access token you gave us, we
|
||||
logged you into GitHub as <code>{gitHubLogin}</code>.
|
||||
</p>
|
||||
<Button onClick={signOut} icon='log-out' text='Sign out'/>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { gitHubToken } = this.props.appState;
|
||||
|
||||
const maybeSignedIn = !!gitHubToken
|
||||
? this.renderSignedIn()
|
||||
: this.renderNotSignedIn();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>GitHub</h4>
|
||||
{maybeSignedIn}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply shows the GitHub Token dialog.``
|
||||
*/
|
||||
private signIn() {
|
||||
this.props.appState.isTokenDialogShowing = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Divider } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
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 {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage GitHub-related preferences.
|
||||
*
|
||||
* @class GitHubSettings
|
||||
* @extends {React.Component<GeneralSettingsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class GeneralSettings extends React.Component<GeneralSettingsProps, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>General Settings</h2>
|
||||
<AppearanceSettings appState={this.props.appState} />
|
||||
<Divider />
|
||||
<ConsoleSettings appState={this.props.appState} />
|
||||
<Divider />
|
||||
<GitHubSettings appState={this.props.appState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
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
|
||||
];
|
||||
|
||||
export interface SettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface SettingsState {
|
||||
section: SettingsSections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything above the editors, so buttons and the address bar.
|
||||
*
|
||||
* @class Settings
|
||||
* @extends {React.Component<SettingsProps, SettingsState>}
|
||||
*/
|
||||
@observer
|
||||
export class Settings extends React.Component<SettingsProps, SettingsState> {
|
||||
constructor(props: SettingsProps) {
|
||||
super(props);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of the settings, usually by simply
|
||||
* return the appropriate component.
|
||||
*
|
||||
* @returns {(JSX.Element | null)}
|
||||
*/
|
||||
public renderContent(): JSX.Element | null {
|
||||
const { section } = this.state;
|
||||
const { appState } = this.props;
|
||||
|
||||
if (section === SettingsSections.General) {
|
||||
return <GeneralSettings appState={appState} />;
|
||||
}
|
||||
|
||||
if (section === SettingsSections.Electron) {
|
||||
return <ElectronSettings appState={appState} />;
|
||||
}
|
||||
|
||||
if (section === SettingsSections.Execution) {
|
||||
return <ExecutionSettings appState={appState} />;
|
||||
}
|
||||
|
||||
if (section === SettingsSections.Credits) {
|
||||
return <CreditsSettings appState={appState} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the individual menu items
|
||||
*
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
public renderOptions(): Array<JSX.Element> {
|
||||
const { section } = this.state;
|
||||
|
||||
return settingsSections.map((name) => {
|
||||
const isSelected = section === name;
|
||||
const onClick = () => this.setState({ section: name });
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={onClick}
|
||||
active={isSelected}
|
||||
key={name}
|
||||
id={`settings-link-${name}`}
|
||||
text={name}
|
||||
icon={this.getIconForSection(name)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { isSettingsShowing } = appState;
|
||||
|
||||
if (!isSettingsShowing) return null;
|
||||
|
||||
return (
|
||||
<div className='settings'>
|
||||
<div className='settings-menu'>
|
||||
<ul>{this.renderOptions()}</ul>
|
||||
</div>
|
||||
<div className='settings-content'>
|
||||
<div className='settings-close' onClick={appState.toggleSettings}>
|
||||
<Icon icon='cross' />
|
||||
</div>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the settings icons
|
||||
*
|
||||
* @param {SettingsSections} section
|
||||
* @memberof Settings
|
||||
*/
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import { Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { Tour, TourScriptStep, TourStepGetButtonParams } from './tour';
|
||||
|
||||
export interface WelcomeTourProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface WelcomeTourState {
|
||||
isTourStarted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is our "Welcome to Electron Fiddle" Tour. It includes both an intro to
|
||||
* the app and a short intro to Electron.
|
||||
*
|
||||
* @returns {Set<TourScriptStep>}
|
||||
*/
|
||||
export function getWelcomeTour(): Set<TourScriptStep> {
|
||||
return new Set([
|
||||
{
|
||||
name: 'fiddle-editors',
|
||||
selector: 'div.mosaic-root',
|
||||
title: '📝 Fiddle Editors',
|
||||
content: (
|
||||
<>
|
||||
<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.
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'select-versions',
|
||||
selector: 'button.version-chooser',
|
||||
title: '📇 Choose an Electron Version',
|
||||
content: (
|
||||
<>
|
||||
<p>
|
||||
Electron Fiddle knows about all released Electron versions, downloading
|
||||
your versions automatically in the background.
|
||||
</p>
|
||||
<p>
|
||||
Open the preferences to see all available versions and delete those previously
|
||||
downloaded.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'button-run',
|
||||
selector: '.button-run',
|
||||
title: '🚀 Run Your Fiddle',
|
||||
content: (
|
||||
<p>
|
||||
Hit this button to give your Fiddle a try and start it.
|
||||
</p>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'button-publish',
|
||||
selector: '.button-publish',
|
||||
title: '🗺 Share Your Fiddle',
|
||||
content: (
|
||||
<>
|
||||
<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.
|
||||
If they don't have Electron Fiddle, they can see and download your code
|
||||
directly from GitHub.
|
||||
</p>
|
||||
<p>
|
||||
You can also package your Fiddle as a standalone binary or as an installer
|
||||
from the "Tasks" menu.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'first-time-electron',
|
||||
selector: 'div.mosaic-root',
|
||||
title: '👋 Getting Started With Electron?',
|
||||
content: (
|
||||
<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} 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: (
|
||||
<>
|
||||
<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>.
|
||||
</p>
|
||||
<p>
|
||||
To get started, pretend that the main process is just like a Node.js
|
||||
process. All APIs and features found in Electron are accessible through
|
||||
the <code>electron</code> module, which can be required like any other
|
||||
Node.js module.
|
||||
</p>
|
||||
<p>
|
||||
The default fiddle creates a new <code>BrowserWindow</code> and loads
|
||||
an HTML file.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'html-editor',
|
||||
selector: 'div.mosaic-window.html',
|
||||
title: '📝 HTML',
|
||||
content: (
|
||||
<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: (
|
||||
<>
|
||||
<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
|
||||
requested a module and install it as soon as you run your fiddle.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The "Welcome to Electron Fiddle" Tour.
|
||||
*
|
||||
* @export
|
||||
* @class WelcomeTour
|
||||
* @extends {React.Component<WelcomeTourProps, WelcomeTourState>}
|
||||
*/
|
||||
@observer
|
||||
export class WelcomeTour extends React.Component<WelcomeTourProps, WelcomeTourState> {
|
||||
constructor(props: WelcomeTourProps) {
|
||||
super(props);
|
||||
|
||||
this.stopTour = this.stopTour.bind(this);
|
||||
this.startTour = this.startTour.bind(this);
|
||||
|
||||
this.state = {
|
||||
isTourStarted: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the tour, closing it.
|
||||
*/
|
||||
public stopTour() {
|
||||
this.props.appState.disableTour();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the tour.
|
||||
*/
|
||||
public startTour() {
|
||||
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;
|
||||
|
||||
if (!isTourShowing) return null;
|
||||
|
||||
if (!isTourStarted) {
|
||||
return (
|
||||
<Dialog
|
||||
key='welcome-tour-dialog'
|
||||
isOpen={true}
|
||||
>
|
||||
<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 {
|
||||
return (
|
||||
<Tour tour={getWelcomeTour()} onStop={this.stopTour} />
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import * as React from 'react';
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
export interface TourProps {
|
||||
tour: Set<TourScriptStep>;
|
||||
onStop: () => void;
|
||||
}
|
||||
|
||||
export interface TourState {
|
||||
tour: IterableIterator<Array<TourScriptStep>>;
|
||||
step: TourScriptStep | null;
|
||||
i: number;
|
||||
}
|
||||
|
||||
export interface TourStepGetButtonParams {
|
||||
stop: () => void;
|
||||
advance: () => void;
|
||||
}
|
||||
|
||||
export class Tour extends React.Component<TourProps, TourState> {
|
||||
private resizeHandle = 0;
|
||||
|
||||
constructor(props: TourProps) {
|
||||
super(props);
|
||||
|
||||
this.advance = this.advance.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
this.onResize = this.onResize.bind(this);
|
||||
|
||||
this.state = {
|
||||
tour: props.tour.entries(),
|
||||
step: null,
|
||||
i: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a resize of the window.
|
||||
*/
|
||||
public onResize() {
|
||||
if (!this.resizeHandle) {
|
||||
this.resizeHandle = window.setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
this.resizeHandle = 0;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component was mounted: Sign up for the window's "resize"
|
||||
* event and move to the first step of the tour.
|
||||
*/
|
||||
public componentDidMount() {
|
||||
this.advance();
|
||||
|
||||
window.addEventListener('resize', this.onResize);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the tour to the next step
|
||||
*/
|
||||
public advance() {
|
||||
const { done, value } = this.state.tour.next();
|
||||
const step = done ? null : value[0];
|
||||
|
||||
this.setState({ step, i: this.state.i + 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the tour
|
||||
*/
|
||||
public stop() {
|
||||
this.props.onStop();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { step } = this.state;
|
||||
|
||||
if (!step) return null;
|
||||
|
||||
return (
|
||||
<div className='tour'>
|
||||
{this.getStep(step)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return buttons for the dialog for the current step
|
||||
* of the tour. By default, we just return a continue/stop
|
||||
* combo
|
||||
*
|
||||
* @param {TourScriptStep} { getButtons }
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
private getButtons({ getButtons }: TourScriptStep): Array<JSX.Element> {
|
||||
// Did the step bring its own buttons?
|
||||
if (getButtons) {
|
||||
return getButtons({
|
||||
stop: this.stop,
|
||||
advance: this.advance
|
||||
});
|
||||
}
|
||||
|
||||
// No? Fine! Are we at the end of the tour?
|
||||
return this.props.tour.size === this.state.i
|
||||
? [(
|
||||
<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'
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the dialog for the current step of the tour.
|
||||
*
|
||||
* @param {TourScriptStep} { content }
|
||||
* @param {ClientRect} rect
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private getDialogForStep(step: TourScriptStep, rect: ClientRect): JSX.Element {
|
||||
const buttons = this.getButtons(step);
|
||||
const size = { width: 400, height: 300 };
|
||||
const margin = 10;
|
||||
const position = positionForRect(rect, size);
|
||||
const style: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
width: size.width - margin,
|
||||
margin: `${margin}px`,
|
||||
zIndex: 1000,
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "mask" for a given step.
|
||||
*
|
||||
* @param {TourScriptStep} { selector, text }
|
||||
* @returns {(JSX.Element | null)}
|
||||
*/
|
||||
private getStep(step: TourScriptStep): JSX.Element | null {
|
||||
const { selector } = step;
|
||||
const element = document.querySelector(selector) || document.body!;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const { top, left, width, height } = rect;
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.getDialogForStep(step, rect)}
|
||||
<svg height='100%' width='100%'>
|
||||
<rect fill='rgba(0, 0, 0, 0.65)' x='0' y='0' mask='url(#mask)' height='100%' width='100%'/>
|
||||
<mask id='mask' maskUnits='userSpaceOnUse' maskContentUnits='userSpaceOnUse'>
|
||||
<rect className='bg' x='0' y='0' fill='white' height='100%' width='100%' />
|
||||
<rect
|
||||
x={left}
|
||||
y={top}
|
||||
width={width}
|
||||
height={height}
|
||||
fill='black'
|
||||
rx='5'
|
||||
ry='5'
|
||||
strokeWidth='5'
|
||||
stroke='black'
|
||||
/>
|
||||
</mask>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as semver from 'semver';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { normalizeVersion } from '../../utils/normalize-version';
|
||||
import { AppState } from '../app';
|
||||
|
||||
export interface VersionChooserState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface VersionChooserProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class VersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
|
||||
constructor(props: VersionChooserProps) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.updateDownloadedVersionState();
|
||||
}
|
||||
|
||||
public async updateDownloadedVersionState() {
|
||||
const { binaryManager } = this.props.appState;
|
||||
const downloadedVersions = await binaryManager.getDownloadedVersions();
|
||||
const updatedVersions = { ...this.props.appState.versions };
|
||||
|
||||
console.log(`Version Chooser: Updating version state`);
|
||||
downloadedVersions.forEach((version) => {
|
||||
if (updatedVersions[version]) {
|
||||
updatedVersions[version].state = 'ready';
|
||||
}
|
||||
});
|
||||
|
||||
this.props.appState.versions = updatedVersions;
|
||||
}
|
||||
|
||||
public handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const version = normalizeVersion(event.target.value);
|
||||
console.log(`Version Chooser: Switching to v${version}`);
|
||||
|
||||
this.props.appState.version = version;
|
||||
|
||||
// Fetch new binaries, maybe?
|
||||
if ((this.props.appState.versions[version] || { state: '' }).state === 'ready') return;
|
||||
|
||||
console.log(`Version Chooser: Instructing BinaryManager to fetch v${version}`);
|
||||
const updatedVersions = { ...this.props.appState.versions };
|
||||
updatedVersions[normalizeVersion(version)].state = 'downloading';
|
||||
this.props.appState.versions = updatedVersions;
|
||||
|
||||
this.props.appState.binaryManager.setup(version)
|
||||
.then(() => this.updateDownloadedVersionState());
|
||||
}
|
||||
|
||||
public renderOptions() {
|
||||
const { versions } = this.props.appState;
|
||||
|
||||
return Object.keys(versions)
|
||||
.sort((a, b) => semver.gt(a, b, true) ? -1 : 1)
|
||||
.map((key) => {
|
||||
const { tag_name, state } = versions[key];
|
||||
const version = tag_name;
|
||||
const icon = state === 'ready'
|
||||
? '✅'
|
||||
: state === 'downloading' ? '⏬' : '⏹';
|
||||
|
||||
return (
|
||||
<option value={version} key={version}>
|
||||
{icon} {version}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<select value={`v${this.props.appState.version}`} onChange={this.handleChange}>
|
||||
{this.renderOptions()}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,23 @@
|
||||
import { remote } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { MosaicNode } from 'react-mosaic-component';
|
||||
|
||||
export const userData = remote.app.getPath('userData');
|
||||
import { EditorId, MosaicId } from '../interfaces';
|
||||
|
||||
// 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 ELECTRON_ORG = 'electron';
|
||||
export const ELECTRON_REPO = 'electron';
|
||||
|
||||
+50
-9
@@ -1,15 +1,56 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { EditorId } from '../interfaces';
|
||||
|
||||
const simpleCache = {};
|
||||
/**
|
||||
* Returns expected content for a given name.
|
||||
*
|
||||
* @export
|
||||
* @param {EditorId} name
|
||||
* @param {string} [version]
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getContent(
|
||||
name: EditorId,
|
||||
version?: string,
|
||||
): Promise<string> {
|
||||
if (name === EditorId.html) {
|
||||
return (await import('../content/html')).html;
|
||||
}
|
||||
|
||||
export function getContent(name: string) {
|
||||
if (simpleCache[name]) return simpleCache[name];
|
||||
if (name === EditorId.renderer) {
|
||||
return (await import('../content/renderer')).renderer;
|
||||
}
|
||||
|
||||
const filePath = path.join(__dirname, '../../static/content', name);
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
if (name === EditorId.main) {
|
||||
// We currently only distinguish between loadFile
|
||||
// and loadURL. TODO: Properly version the quick-start.
|
||||
if (version && version.startsWith('1.')) {
|
||||
return (await import('../content/main-1-x-x')).main;
|
||||
}
|
||||
|
||||
simpleCache[name] = content;
|
||||
return (await import('../content/main')).main;
|
||||
}
|
||||
|
||||
return content;
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Did the content change?
|
||||
*
|
||||
* @param {EditorId} name
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function isContentUnchanged(name: EditorId): Promise<boolean> {
|
||||
if (!window.ElectronFiddle || !window.ElectronFiddle.app) return false;
|
||||
|
||||
const values = await window.ElectronFiddle.app.getEditorValues({ include: false });
|
||||
|
||||
// Handle main case, which needs to check both possible versions
|
||||
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 {
|
||||
return values[name] === await getContent(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import * as fsType from 'fs-extra';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ElectronVersion, ElectronVersionSource } from '../interfaces';
|
||||
import { callIn } from '../utils/call-in';
|
||||
import { fancyImport } from '../utils/import';
|
||||
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 async function fetchTypeDefinitions(version: string): Promise<string> {
|
||||
const url = `https://unpkg.com/electron@${version}/electron.d.ts`;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path for offline TypeScript definitions
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getOfflineTypeDefinitionPath(version: string): string {
|
||||
return path.join(definitionPath, version, 'electron.d.ts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Do TypeScript definitions for the current version of Electron exist on disk?
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export async function getOfflineTypeDefinitions(version: string): Promise<boolean> {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
return fs.existsSync(getOfflineTypeDefinitionPath(version));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TypeScript definitions for a version of Electron. If none can be
|
||||
* found, returns null.
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function getDownloadedVersionTypeDefs(version: ElectronVersion): Promise<string | null> {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
await fs.mkdirp(definitionPath);
|
||||
const offlinePath = getOfflineTypeDefinitionPath(version.version);
|
||||
|
||||
if (await getOfflineTypeDefinitions(version.version)) {
|
||||
try {
|
||||
return await fs.readFile(offlinePath, 'utf-8');
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
const typeDefs = await fetchTypeDefinitions(version.version);
|
||||
if (typeDefs && typeDefs.length > 0) {
|
||||
try {
|
||||
await fs.outputFile(offlinePath, typeDefs);
|
||||
|
||||
return typeDefs;
|
||||
} catch (error) {
|
||||
console.warn(`Fetch Types: Could not write to disk`, error);
|
||||
return typeDefs;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLocalVersionTypeDefs(version: ElectronVersion) {
|
||||
if (version.source === ElectronVersionSource.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: ElectronVersion, i: number = 0): Promise<void> {
|
||||
const defer = async (): Promise<void> => {
|
||||
if (i > 10) {
|
||||
console.warn(`Fetch Types: Failed, dependencies do not exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(`Fetch Types: Called too soon, deferring`);
|
||||
return callIn(i * 100 + 200, () => updateEditorTypeDefinitions(version, i + 1));
|
||||
};
|
||||
|
||||
// If this method is called before we're ready, we'll delay this work a bit
|
||||
if (!window.ElectronFiddle.app || !window.ElectronFiddle.app.monaco) return defer();
|
||||
|
||||
const { app } = window.ElectronFiddle;
|
||||
const monaco: typeof MonacoType = app.monaco!;
|
||||
const typeDefDisposable: MonacoType.IDisposable = app.typeDefDisposable!;
|
||||
|
||||
const getTypeDefs = (version.source === ElectronVersionSource.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.version}`);
|
||||
const disposable = monaco.languages.typescript.javascriptDefaults.addExtraLib(typeDefs);
|
||||
window.ElectronFiddle.app.typeDefDisposable = disposable;
|
||||
} else {
|
||||
console.log(`Fetch Types: No type definitions for ${version.version} 😢`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalTypePathForVersion(version: ElectronVersion) {
|
||||
if (version.localPath) {
|
||||
return path.join(
|
||||
version.localPath,
|
||||
'gen',
|
||||
'electron',
|
||||
'tsc',
|
||||
'typings',
|
||||
'electron.d.ts'
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user