Compare commits
1223
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bc8389c6c | ||
|
|
2cb8e23bc2 | ||
|
|
8afff13f8d | ||
|
|
0abc4fb9a9 | ||
|
|
5f2563b39f | ||
|
|
889528c4d1 | ||
|
|
7cd6908009 | ||
|
|
e3e148d1e8 | ||
|
|
1448bc6d55 | ||
|
|
ea92d8130f | ||
|
|
6e54dd6493 | ||
|
|
24435b3f1a | ||
|
|
973cf1bd2d | ||
|
|
2a0160f4f9 | ||
|
|
fccc7831db | ||
|
|
a1ec0bf259 | ||
|
|
54c02ac35d | ||
|
|
ac05d578ce | ||
|
|
f1cf7db354 | ||
|
|
6e40af3bf4 | ||
|
|
424d87b63d | ||
|
|
33c0832e83 | ||
|
|
1a0e21d9fa | ||
|
|
5044ca7f31 | ||
|
|
23900196e7 | ||
|
|
604f3190b7 | ||
|
|
fc3ed6f4f6 | ||
|
|
c5d0e3b156 | ||
|
|
7705fdc80c | ||
|
|
a436b021b0 | ||
|
|
f9fe9dba84 | ||
|
|
7bab157863 | ||
|
|
ac6c7cde1c | ||
|
|
1dc64f4c53 | ||
|
|
836a80c6f1 | ||
|
|
4825900ae0 | ||
|
|
6f2218e239 | ||
|
|
ca1c49a238 | ||
|
|
d346a91af9 | ||
|
|
e675f8f482 | ||
|
|
eb03608473 | ||
|
|
d3b10ee773 | ||
|
|
2a75120dd9 | ||
|
|
7177bce5f8 | ||
|
|
bfb54690b9 | ||
|
|
b5a572bbae | ||
|
|
cd2e719994 | ||
|
|
b5ab9c1a75 | ||
|
|
0c4a09cca6 | ||
|
|
2a67e7e6cf | ||
|
|
351c5caf23 | ||
|
|
36780a95e7 | ||
|
|
9065a8678a | ||
|
|
016d3ee78a | ||
|
|
4b40420bba | ||
|
|
0e872b0dd0 | ||
|
|
7638111598 | ||
|
|
a0e59ad288 | ||
|
|
3c6103dc50 | ||
|
|
b98472f973 | ||
|
|
3602eacc88 | ||
|
|
8b84c040c7 | ||
|
|
6aa19eada6 | ||
|
|
8e92183de2 | ||
|
|
1a951f3f44 | ||
|
|
66ead93f05 | ||
|
|
d22899b4a5 | ||
|
|
77d4bc6fc5 | ||
|
|
961d42d74f | ||
|
|
c58ade2132 | ||
|
|
592c220f18 | ||
|
|
4da0e82e9b | ||
|
|
f766bef10c | ||
|
|
30ba9fb023 | ||
|
|
f3e847da85 | ||
|
|
0f854441e4 | ||
|
|
f31a4e38bb | ||
|
|
26bb3e4944 | ||
|
|
fb1fb7869c | ||
|
|
a1e67400bf | ||
|
|
a26aa1e70c | ||
|
|
78395412f4 | ||
|
|
1f72db3cfc | ||
|
|
c333a1e132 | ||
|
|
a0a989d7a7 | ||
|
|
5b07b8a706 | ||
|
|
98a358b756 | ||
|
|
9b55b2ef26 | ||
|
|
5b91177348 | ||
|
|
f8c43a07f7 | ||
|
|
5eba9cdbe2 | ||
|
|
8ee42c290e | ||
|
|
c2b3954dfa | ||
|
|
e61599e9c5 | ||
|
|
1fed772a27 | ||
|
|
de85d0f324 | ||
|
|
c89671d99d | ||
|
|
155b901ade | ||
|
|
8e098deab8 | ||
|
|
2b5ea7d467 | ||
|
|
91efa5a009 | ||
|
|
4ec5bec719 | ||
|
|
84ac41f147 | ||
|
|
e9a2985291 | ||
|
|
9b28919203 | ||
|
|
8d43836787 | ||
|
|
48d0dadf5d | ||
|
|
2f9ef21251 | ||
|
|
8e4238275c | ||
|
|
32d5ea99aa | ||
|
|
da2e17cd37 | ||
|
|
539f5bce97 | ||
|
|
228552c8a0 | ||
|
|
ed8d76a54a | ||
|
|
2f4d47a0ab | ||
|
|
9b443de137 | ||
|
|
efb5bf0e57 | ||
|
|
c420747bb9 | ||
|
|
33223e7b37 | ||
|
|
86cc0c62a1 | ||
|
|
96bd2e36dd | ||
|
|
9db527773b | ||
|
|
9da1acafd9 | ||
|
|
5a05d34776 | ||
|
|
6b9d2b66e2 | ||
|
|
d4096a6de8 | ||
|
|
3709d122a6 | ||
|
|
0368cda57a | ||
|
|
433cb6acd5 | ||
|
|
56dceaefb0 | ||
|
|
80da23c4e4 | ||
|
|
353968ef8e | ||
|
|
64ece1b364 | ||
|
|
13383f7345 | ||
|
|
cae7222636 | ||
|
|
2aabe80f2b | ||
|
|
d0683f1392 | ||
|
|
389352cefa | ||
|
|
d04b654c3e | ||
|
|
1369c9b35c | ||
|
|
9957090d25 | ||
|
|
8a002fb0a8 | ||
|
|
cae8dde4c8 | ||
|
|
cb176534f9 | ||
|
|
f7fa9a8ffc | ||
|
|
347384a547 | ||
|
|
e272efb3b9 | ||
|
|
5255bcb842 | ||
|
|
d3cd53f3e0 | ||
|
|
6d28c4c81b | ||
|
|
a45354e1af | ||
|
|
28245d36ba | ||
|
|
582ef280b5 | ||
|
|
56b7f2ffb7 | ||
|
|
cb3ba4f0a7 | ||
|
|
e238b10f1e | ||
|
|
bbc22d9ca8 | ||
|
|
7b3c960c1f | ||
|
|
4dbc74fccb | ||
|
|
f71b1bc8d4 | ||
|
|
00c2e1f9a6 | ||
|
|
50f69613dd | ||
|
|
4a23b53b1b | ||
|
|
dcd21935a9 | ||
|
|
ff37d5cfb9 | ||
|
|
bb8053d86d | ||
|
|
5efaa54255 | ||
|
|
b38507230d | ||
|
|
7451083c70 | ||
|
|
be46780674 | ||
|
|
99b5d067df | ||
|
|
f34eb225ea | ||
|
|
18cc36a837 | ||
|
|
da403e3cbd | ||
|
|
b4431cf426 | ||
|
|
e714269b9b | ||
|
|
b9518a63f8 | ||
|
|
0627801bb0 | ||
|
|
3adeece64d | ||
|
|
aed45078c5 | ||
|
|
55bfb0241b | ||
|
|
7e90679706 | ||
|
|
4897bdbf84 | ||
|
|
7a02f8cacd | ||
|
|
a65d594fbc | ||
|
|
f32fce2f80 | ||
|
|
26e054702a | ||
|
|
16f27132cb | ||
|
|
7613176f69 | ||
|
|
470bea4fca | ||
|
|
363febdb73 | ||
|
|
062ca4e205 | ||
|
|
49053342a5 | ||
|
|
fd582c67a8 | ||
|
|
706b18808b | ||
|
|
a6ae8d5376 | ||
|
|
5034f8c853 | ||
|
|
e916247013 | ||
|
|
c052686de4 | ||
|
|
31aacef131 | ||
|
|
932369b705 | ||
|
|
b835c34786 | ||
|
|
c9e1ba815a | ||
|
|
374a85e2f6 | ||
|
|
4282d0b93d | ||
|
|
549862b3bb | ||
|
|
4691c1b494 | ||
|
|
8125abc657 | ||
|
|
8cba7739f5 | ||
|
|
0b39fe40b1 | ||
|
|
cae078ffe6 | ||
|
|
bfb931435f | ||
|
|
c878f72f2e | ||
|
|
32494c057a | ||
|
|
e732685fff | ||
|
|
d9d5e0cbc9 | ||
|
|
0e1d7b541e | ||
|
|
8a602d94b7 | ||
|
|
155889dbcc | ||
|
|
56ac6662b6 | ||
|
|
adb196549b | ||
|
|
999facf062 | ||
|
|
fa0115f2db | ||
|
|
c0a4c0e48d | ||
|
|
3a5d2205fe | ||
|
|
1083b8f859 | ||
|
|
252d4c1de6 | ||
|
|
0c44d2c9e4 | ||
|
|
5eebc06514 | ||
|
|
eac1423177 | ||
|
|
ae88780281 | ||
|
|
4bf9d48d70 | ||
|
|
df6d4b5a9a | ||
|
|
68202e2b23 | ||
|
|
acd5b35ca1 | ||
|
|
fae1ca3138 | ||
|
|
bd220d2690 | ||
|
|
d31f1016b8 | ||
|
|
3b52be0f79 | ||
|
|
0e11c56f7f | ||
|
|
8019981d1a | ||
|
|
403a58e216 | ||
|
|
3341d0ccaa | ||
|
|
d42e83100a | ||
|
|
50e9883d82 | ||
|
|
f5aedac6a3 | ||
|
|
a3f4aa8ed3 | ||
|
|
9c839aaad8 | ||
|
|
3a7ca8ccd5 | ||
|
|
fef42293bc | ||
|
|
1f9db42c80 | ||
|
|
8e5657ef2d | ||
|
|
a94105dd27 | ||
|
|
cf214dc0d0 | ||
|
|
6dfdf00437 | ||
|
|
b3743a4c73 | ||
|
|
3c0d6c2293 | ||
|
|
89b589fd2e | ||
|
|
42c577988d | ||
|
|
d0e47b6b4d | ||
|
|
f44a127012 | ||
|
|
fa2f5454ce | ||
|
|
1021bb8b2b | ||
|
|
cf4ad36ca2 | ||
|
|
4fe9c627b5 | ||
|
|
262e636e8d | ||
|
|
bbe79ed6e2 | ||
|
|
bc96a5013a | ||
|
|
66ba7b74d5 | ||
|
|
38e5aa98e3 | ||
|
|
a91504b173 | ||
|
|
4685e88c86 | ||
|
|
e0bb82d919 | ||
|
|
7035be3286 | ||
|
|
c961cfadb8 | ||
|
|
7267a2ee88 | ||
|
|
bc03382dc5 | ||
|
|
891c33ba68 | ||
|
|
e7260c74a4 | ||
|
|
ed694a7044 | ||
|
|
14fc77c08c | ||
|
|
f3079648cf | ||
|
|
7272ec7899 | ||
|
|
ac9ac701f7 | ||
|
|
7fce572823 | ||
|
|
6186a80dad | ||
|
|
ade6576285 | ||
|
|
61c58277f3 | ||
|
|
c361d34a6d | ||
|
|
5887a6615a | ||
|
|
7ba4cf5be7 | ||
|
|
0daf872d2b | ||
|
|
75ec8ee9b1 | ||
|
|
183c152e40 | ||
|
|
d6a807ba41 | ||
|
|
24e0abf667 | ||
|
|
eb8261e240 | ||
|
|
84cc07da90 | ||
|
|
b0f3837b47 | ||
|
|
e29664ca45 | ||
|
|
ef36ee39f6 | ||
|
|
73dadb815e | ||
|
|
4945501703 | ||
|
|
77c87fbdeb | ||
|
|
2ef5b42f4a | ||
|
|
9c4bc2ba52 | ||
|
|
80da4e73e3 | ||
|
|
eadd58d149 | ||
|
|
1fc82f8f6d | ||
|
|
9bd7043b2c | ||
|
|
aaeef6fd06 | ||
|
|
1195902bd5 | ||
|
|
c022d0f33d | ||
|
|
996ae775f3 | ||
|
|
ed44dda8fb | ||
|
|
effba13e73 | ||
|
|
65886c3b52 | ||
|
|
8bd4dd366a | ||
|
|
4e19578e34 | ||
|
|
c7fd0bc95e | ||
|
|
29e2f03b1a | ||
|
|
4cd6f330da | ||
|
|
79521eac7c | ||
|
|
2736499d68 | ||
|
|
1cbb781e8d | ||
|
|
e849a49f6f | ||
|
|
0114d2c1eb | ||
|
|
365537012f | ||
|
|
3e435cb64d | ||
|
|
6981382793 | ||
|
|
f293ba84d1 | ||
|
|
55cf507a96 | ||
|
|
e71c2439cd | ||
|
|
9bd253951c | ||
|
|
eda08c9aea | ||
|
|
9414f66893 | ||
|
|
f4725b5dd8 | ||
|
|
fd8be47494 | ||
|
|
2cebe2ca7c | ||
|
|
74135bab67 | ||
|
|
a856340d01 | ||
|
|
bd8939c358 | ||
|
|
9d3f3d61eb | ||
|
|
bdeaffcb7d | ||
|
|
344e1df375 | ||
|
|
d5b52dcae7 | ||
|
|
0faeb24761 | ||
|
|
284b579d0c | ||
|
|
5a1c24cdda | ||
|
|
024d48a273 | ||
|
|
32155d652d | ||
|
|
874a8bb0f0 | ||
|
|
73470640c7 | ||
|
|
679f731f4c | ||
|
|
a41ad11f8c | ||
|
|
7cc6d38dc2 | ||
|
|
1e6ee7ab37 | ||
|
|
c50c31ad80 | ||
|
|
5f8557fb13 | ||
|
|
47b20fa3e4 | ||
|
|
b5c8ed60a9 | ||
|
|
5e3d382b85 | ||
|
|
fed0107dd1 | ||
|
|
ca9b9c633a | ||
|
|
fb4c6b3f33 | ||
|
|
aa51c0cc5c | ||
|
|
e5236c5500 | ||
|
|
321f7a8bbc | ||
|
|
1ff7a3fe16 | ||
|
|
a1c74c018d | ||
|
|
5df39ea51a | ||
|
|
8a9591be9f | ||
|
|
3cc8fd41f6 | ||
|
|
25df3dd989 | ||
|
|
abe7bdab23 | ||
|
|
16bea01ba5 | ||
|
|
7417a82931 | ||
|
|
4c41db8595 | ||
|
|
5da87ffca3 | ||
|
|
919e9cb6b6 | ||
|
|
14279283d6 | ||
|
|
77a98112a1 | ||
|
|
08fb3f52bf | ||
|
|
b37ba895e5 | ||
|
|
33a236edd1 | ||
|
|
f7930fb523 | ||
|
|
88cad66076 | ||
|
|
f0afedf896 | ||
|
|
38234dd0a3 | ||
|
|
83e834bea5 | ||
|
|
38be3cca14 | ||
|
|
4908cc6110 | ||
|
|
ab4a9015d1 | ||
|
|
73ab813efc | ||
|
|
f3570c5069 | ||
|
|
4c564eaa63 | ||
|
|
a289842e32 | ||
|
|
9d697e7a3f | ||
|
|
e96e5345e6 | ||
|
|
7c97bc2ef6 | ||
|
|
d55c5c067d | ||
|
|
083ac3fbc2 | ||
|
|
5c8fe3d6e0 | ||
|
|
f034c5240c | ||
|
|
6421187db4 | ||
|
|
6fb385a95f | ||
|
|
c47fea09ed | ||
|
|
ef17ccd9cb | ||
|
|
3f173aa4d4 | ||
|
|
faf15081e0 | ||
|
|
4306f53c70 | ||
|
|
52a8aac6c7 | ||
|
|
7660192dcb | ||
|
|
53ceeb5600 | ||
|
|
5cd7a57f08 | ||
|
|
e96a3da09b | ||
|
|
90c198d487 | ||
|
|
5b56333516 | ||
|
|
c87f65ca76 | ||
|
|
0f27c8aa7a | ||
|
|
b5880aef46 | ||
|
|
fb46eaf0e4 | ||
|
|
c1040097e3 | ||
|
|
333758b73d | ||
|
|
3c2a4e1b76 | ||
|
|
f3d42c107f | ||
|
|
2ece4dc728 | ||
|
|
dd2975bd0e | ||
|
|
fdf03dff9b | ||
|
|
48fa762f7f | ||
|
|
7cd9a86920 | ||
|
|
db2caaad59 | ||
|
|
3b2cb93d8b | ||
|
|
9822753ffd | ||
|
|
8712907182 | ||
|
|
40055ee069 | ||
|
|
a33f175867 | ||
|
|
107ba63a5a | ||
|
|
b66ad7e247 | ||
|
|
7eb15b6666 | ||
|
|
2ed29f68dd | ||
|
|
d89d595b57 | ||
|
|
2b34ef5e67 | ||
|
|
d6f7146447 | ||
|
|
006ba5f0a5 | ||
|
|
5df390b0fd | ||
|
|
3eb25fed91 | ||
|
|
9b06f792ea | ||
|
|
c116152605 | ||
|
|
b9f8d55809 | ||
|
|
89e697fcd6 | ||
|
|
9df5314c08 | ||
|
|
d01ba1cdff | ||
|
|
ed5ccf133f | ||
|
|
e1decd4819 | ||
|
|
3c9f477b89 | ||
|
|
5cfbf2cab5 | ||
|
|
89272fbaa9 | ||
|
|
5d572f451c | ||
|
|
319136ecc8 | ||
|
|
3a4aa06331 | ||
|
|
e283a118f6 | ||
|
|
e67d1b0179 | ||
|
|
9805d98dd8 | ||
|
|
009ad25983 | ||
|
|
9a65124289 | ||
|
|
4d7c81ee69 | ||
|
|
ed91cb5ba1 | ||
|
|
8b04f67f71 | ||
|
|
2e183f0538 | ||
|
|
6b022bbea7 | ||
|
|
6bdb7d71ec | ||
|
|
0ebe6ab9b3 | ||
|
|
5e73deeb4f | ||
|
|
02d81aa407 | ||
|
|
46deeb857e | ||
|
|
ca55d2e620 | ||
|
|
88974d277f | ||
|
|
1239afafeb | ||
|
|
415877c067 | ||
|
|
0a0779ffa1 | ||
|
|
9cf34ea983 | ||
|
|
83a7d23b38 | ||
|
|
7d19ad91a6 | ||
|
|
db13699790 | ||
|
|
4a8fc24ff6 | ||
|
|
3ed41626b1 | ||
|
|
b3e25cfe79 | ||
|
|
00bb15a891 | ||
|
|
94da51128a | ||
|
|
b573f09494 | ||
|
|
f05e36de34 | ||
|
|
44c7c70793 | ||
|
|
bcbca7aceb | ||
|
|
9515458545 | ||
|
|
219e90cad8 | ||
|
|
ba23c8019d | ||
|
|
2fef96a2b2 | ||
|
|
2d8cb9748d | ||
|
|
eaefbb7ad0 | ||
|
|
d5f28253f8 | ||
|
|
ac451d6b4a | ||
|
|
b5e0e63766 | ||
|
|
49538ea55a | ||
|
|
23ac9b8e02 | ||
|
|
4bf2bee7c8 | ||
|
|
e742d8426c | ||
|
|
7a223fd3dc | ||
|
|
6bdc995eb6 | ||
|
|
72cbcd46d1 | ||
|
|
f1e51c71a3 | ||
|
|
95e88c0eee | ||
|
|
a54037ef0e | ||
|
|
83bb6fc73e | ||
|
|
f8f1f0ec08 | ||
|
|
f7a19b92a8 | ||
|
|
dc76b78337 | ||
|
|
f6f3e94a97 | ||
|
|
89cede25b1 | ||
|
|
8e8eec3cc6 | ||
|
|
a0d8ca4019 | ||
|
|
1f0581afbc | ||
|
|
70d4902b87 | ||
|
|
a93a99e3f0 | ||
|
|
4c209159e6 | ||
|
|
b09a248c96 | ||
|
|
6f09ed9c2a | ||
|
|
c1ae0ecd86 | ||
|
|
1e9a81fcf4 | ||
|
|
2061b26ed6 | ||
|
|
88ce4a76e9 | ||
|
|
9b4ef4c0d2 | ||
|
|
dca2cd67e5 | ||
|
|
6bab353088 | ||
|
|
e6cc44ca37 | ||
|
|
d039c5dbf5 | ||
|
|
c155fff8b0 | ||
|
|
11202a8d6b | ||
|
|
475127f5b1 | ||
|
|
feee5be2f8 | ||
|
|
678c7f1578 | ||
|
|
d2fcb8071b | ||
|
|
0ab35dc867 | ||
|
|
377d221191 | ||
|
|
cbdab9d662 | ||
|
|
6875ab392e | ||
|
|
58ec0f015c | ||
|
|
2aef0e776e | ||
|
|
b2a7d6b2a4 | ||
|
|
303d6bc79a | ||
|
|
d028e7c915 | ||
|
|
f04631b6c1 | ||
|
|
c1bb524b59 | ||
|
|
4a38fda293 | ||
|
|
b98ad2b572 | ||
|
|
6118b94b09 | ||
|
|
d0c1ee1a82 | ||
|
|
525715a911 | ||
|
|
afbc0c9e6c | ||
|
|
c1a9cdf88b | ||
|
|
d281e61b8a | ||
|
|
2fbbc10497 | ||
|
|
e8226ca97d | ||
|
|
08f2dbe937 | ||
|
|
68813edb51 | ||
|
|
a32261414b | ||
|
|
42f67e7b21 | ||
|
|
7d9b0ed79d | ||
|
|
47f1b5c4c3 | ||
|
|
432322694d | ||
|
|
889e76cbae | ||
|
|
4a689e5c06 | ||
|
|
3113f324cc | ||
|
|
64addb1fb0 | ||
|
|
e6facf60c1 | ||
|
|
00cb5cf6c6 | ||
|
|
3e8dbebe42 | ||
|
|
dcdef4aa4e | ||
|
|
590389abdc | ||
|
|
18a8f0c142 | ||
|
|
a8e8ab6f04 | ||
|
|
45ed35467a | ||
|
|
eb49cb296a | ||
|
|
c2344d50d2 | ||
|
|
7786e113d9 | ||
|
|
f8b51870d8 | ||
|
|
601c28dcf8 | ||
|
|
49379af856 | ||
|
|
8347f79756 | ||
|
|
c102128d5a | ||
|
|
fcff053136 | ||
|
|
c3641cfa04 | ||
|
|
6eba3cbdcc | ||
|
|
6393c59d5c | ||
|
|
543e86bc91 | ||
|
|
ea5fdd426c | ||
|
|
fecc499cc5 | ||
|
|
8d0a1524b9 | ||
|
|
918e91965a | ||
|
|
15bab9ff86 | ||
|
|
7d879b59cf | ||
|
|
72f7e058d6 | ||
|
|
a65e044b5c | ||
|
|
a4c4590816 | ||
|
|
6fbb816be3 | ||
|
|
ae0ba026f2 | ||
|
|
cc2a44d10a | ||
|
|
3e6eb16974 | ||
|
|
4212284fc4 | ||
|
|
8b35ff8f95 | ||
|
|
4d00ee9fd4 | ||
|
|
590db5304d | ||
|
|
e519ce2f13 | ||
|
|
8221920fab | ||
|
|
5de7ed7d9d | ||
|
|
a1586e864a | ||
|
|
60745ff8bb | ||
|
|
4ac20aa5c2 | ||
|
|
48faec9741 | ||
|
|
c0f4e95a67 | ||
|
|
d5c8c8bd96 | ||
|
|
80ff396879 | ||
|
|
bf42abc88d | ||
|
|
eaa064ea8d | ||
|
|
e804436669 | ||
|
|
08d07b34a4 | ||
|
|
74ba7db13c | ||
|
|
0fced3aefd | ||
|
|
13c9217f9f | ||
|
|
346742b242 | ||
|
|
aefe273516 | ||
|
|
337d19c33a | ||
|
|
ebc6f91dea | ||
|
|
25446aa9e4 | ||
|
|
f2859c053b | ||
|
|
49cd192817 | ||
|
|
9bdd09c6b5 | ||
|
|
c3d36b29b0 | ||
|
|
1b69a2ff52 | ||
|
|
311497d080 | ||
|
|
55b7739d36 | ||
|
|
0fda97eb2f | ||
|
|
879815fc02 | ||
|
|
c469c3e856 | ||
|
|
1b6750e0c5 | ||
|
|
f14bd5c041 | ||
|
|
76115a3e9c | ||
|
|
65f86e0177 | ||
|
|
18cacb1532 | ||
|
|
6a924ac88b | ||
|
|
d125af6775 | ||
|
|
1a9a0579a5 | ||
|
|
acee131288 | ||
|
|
66536a5260 | ||
|
|
7941a83109 | ||
|
|
b312b80b69 | ||
|
|
b3c3b73b7d | ||
|
|
0a8e9cde1b | ||
|
|
ca3f48697d | ||
|
|
067e5cd5e2 | ||
|
|
7e16c8c1f2 | ||
|
|
e932aad47b | ||
|
|
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,4 @@
|
||||
/out
|
||||
/dist
|
||||
/coverage
|
||||
/static
|
||||
@@ -0,0 +1,30 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
|
||||
sourceType: 'module', // Allows for the use of imports
|
||||
ecmaFeatures: {
|
||||
jsx: true, // Allows for the parsing of JSX
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react
|
||||
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
|
||||
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
|
||||
],
|
||||
rules: {
|
||||
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
|
||||
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
@@ -0,0 +1,31 @@
|
||||
const { Octokit } = require('@octokit/action')
|
||||
|
||||
const octokit = new Octokit()
|
||||
|
||||
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
|
||||
|
||||
async function main () {
|
||||
const prs = await octokit.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
head: `${owner}:update-releases`
|
||||
})
|
||||
|
||||
if (prs.data.length === 0) {
|
||||
const pr = await octokit.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title: 'build: update Electron releases JSON',
|
||||
base: 'master',
|
||||
head: 'update-releases',
|
||||
body: 'Auto-update from GitHub Actions.',
|
||||
maintainer_can_modify: true
|
||||
})
|
||||
console.log('Pull request created:', pr.html_url)
|
||||
} else {
|
||||
console.log('Pull request updated:', prs.data[0].html_url)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Auto-update Releases JSON file
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
jobs:
|
||||
autoupdate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Fetch git branches
|
||||
run: git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Get npm cache directory
|
||||
id: npm-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(npm config get cache)"
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.npm-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- run: npm ci
|
||||
- name: Switch to release update branch
|
||||
run: |
|
||||
if git branch --remotes | grep -q origin/update-releases; then
|
||||
git checkout update-releases
|
||||
else
|
||||
git checkout -b update-releases
|
||||
fi
|
||||
- name: Update Releases JSON
|
||||
run: npm run electron-releases
|
||||
- name: Commit Changes to Releases JSON
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "machine github.com login $GITHUB_ACTOR password $GITHUB_TOKEN" > ~/.netrc
|
||||
chmod 600 ~/.netrc
|
||||
git add static/releases.json tests/fixtures/releases-metadata.json
|
||||
if test -n "$(git status -s)"; then
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "electron-bot@users.noreply.github.com"
|
||||
git diff --cached
|
||||
git commit -m "build: update Electron releases JSON"
|
||||
git push origin update-releases
|
||||
node --unhandled-rejections=strict .github/actions/create_releases_pr.js
|
||||
else
|
||||
echo No update needed
|
||||
fi
|
||||
+43
-3
@@ -1,3 +1,43 @@
|
||||
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
|
||||
|
||||
# Test report
|
||||
report.json
|
||||
|
||||
# npm package
|
||||
/npm/dist
|
||||
/npm/path.txt
|
||||
|
||||
# Electron Fiddle-Specific
|
||||
/static/contributors.json
|
||||
/static/css/root.css
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
trailingComma: "all",
|
||||
singleQuote: true,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"ignoreFiles": [
|
||||
"./src/less/*-vendor.less"
|
||||
],
|
||||
"rules": { }
|
||||
}
|
||||
+38
-8
@@ -1,15 +1,14 @@
|
||||
language: node_js
|
||||
node_js: "8"
|
||||
node_js: "12"
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
dist: trusty
|
||||
osx_image: xcode8.3
|
||||
sudo: false
|
||||
dist: bionic
|
||||
osx_image: xcode10
|
||||
|
||||
cache:
|
||||
npm: true
|
||||
directories:
|
||||
- node_modules
|
||||
- $HOME/.cache/electron
|
||||
|
||||
addons:
|
||||
@@ -24,10 +23,41 @@ branches:
|
||||
- /^v\d+\.\d+\.\d+/
|
||||
|
||||
install:
|
||||
- npm install
|
||||
- npm update
|
||||
- npm ci
|
||||
- |
|
||||
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/
|
||||
@@ -0,0 +1,74 @@
|
||||
# Contributing to Electron Fiddle
|
||||
|
||||
Electron Fiddle is a community-driven project, overseen by the [Electron Ecosystem Working
|
||||
Group](https://github.com/electron/governance/tree/master/wg-ecosystem#readme). As such, we welcome
|
||||
and encourage all sorts of contributions. They include, but are not limited to:
|
||||
|
||||
- Constructive feedback
|
||||
- Bug reports / technical issues
|
||||
- Documentation changes
|
||||
- Feature requests
|
||||
- [Pull requests](#filing-pull-requests)
|
||||
|
||||
We strongly suggest that before filing an issue, you search through the existing issues to see
|
||||
if it has already been filed by someone else.
|
||||
|
||||
This project is a part of the Electron ecosystem. As such, all contributions to this project follow
|
||||
[Electron's code of conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md)
|
||||
where appropriate.
|
||||
|
||||
## Contribution Suggestions
|
||||
|
||||
We use the label [`good first issue`](https://github.com/electron/fiddle/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) in the issue tracker to denote fairly-well-scoped-out bugs or feature requests that the community can pick up and work on. If any of those labeled issues do not have enough information, please feel free to ask constructive questions. (This applies to any open issue.)
|
||||
|
||||
## Filing Pull Requests
|
||||
|
||||
Here are some things to keep in mind as you file pull requests to fix bugs, add new features, etc.:
|
||||
|
||||
* If you're unfamiliar with forking, branching, and pull requests, please see [GitHub's extensive
|
||||
documentation](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests).
|
||||
* Travis CI and AppVeyor are used to make sure that any new or changed code meets the project's
|
||||
style guidelines, and that the project's testsuite passes for each new commit.
|
||||
* Unless it's impractical, please write tests for your changes. This will help us so that we can
|
||||
spot regressions much easier.
|
||||
* When creating commit messages and pull request titles, please adhere to the [conventional
|
||||
commits](https://www.conventionalcommits.org/en/v1.0.0/) standard.
|
||||
* Please **do not** bump the version number in your pull requests, the maintainers will do that.
|
||||
Feel free to indicate whether the changes require a major, minor, or patch version bump, as
|
||||
prescribed by the [semantic versioning specification](http://semver.org/).
|
||||
|
||||
### Running Fiddle From Source
|
||||
|
||||
1. Clone the Electron Fiddle repository locally.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/electron/fiddle.git
|
||||
```
|
||||
|
||||
2. Change directory to where Fiddle has been cloned.
|
||||
|
||||
```sh
|
||||
cd fiddle
|
||||
```
|
||||
|
||||
3. Install dependencies.
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Start Fiddle and Explore!
|
||||
|
||||
```sh
|
||||
npm start
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```sh
|
||||
npm test
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
<!-- TODO @felix add your release process here 😁 -->
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2013-2018 GitHub Inc.
|
||||
Copyright (c) 2013-2020 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@@ -1,5 +1,74 @@
|
||||
# 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)
|
||||
[](https://discord.com/invite/electron)
|
||||
|
||||

|
||||
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!
|
||||
|
||||
## Contributing
|
||||
|
||||
Electron Fiddle is a community-driven project that welcomes all sorts of contributions. Please check out our [Contributing Guide](https://github.com/electron/fiddle/blob/master/CONTRIBUTING.md) for more details.
|
||||
|
||||
## License
|
||||
|
||||
[MIT, please see the LICENSE file for full details](https://github.com/electron/fiddle/blob/master/LICENSE.md).
|
||||
|
||||
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 |
+130
-22
@@ -1,27 +1,94 @@
|
||||
/* tslint:disable */
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {},
|
||||
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'),
|
||||
appBundleId: 'com.electron.fiddle',
|
||||
usageDescription: {
|
||||
Camera:
|
||||
'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Camera',
|
||||
Microphone:
|
||||
'Access is needed by certain built-in fiddles in addition to any custom fiddles that use the Microphone',
|
||||
},
|
||||
appCategoryType: 'public.app-category.developer-tools',
|
||||
protocols: [
|
||||
{
|
||||
name: 'Electron Fiddle Launch Protocol',
|
||||
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"],
|
||||
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-deb',
|
||||
platforms: ['linux'],
|
||||
config: {
|
||||
name: "electron_fiddle"
|
||||
}
|
||||
icon: {
|
||||
scalable: path.resolve(iconDir, 'fiddle.svg'),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "@electron-forge/maker-zip",
|
||||
platforms: ["darwin"]
|
||||
name: '@electron-forge/maker-rpm',
|
||||
platforms: ['linux'],
|
||||
},
|
||||
{
|
||||
name: "@electron-forge/maker-deb",
|
||||
platforms: ["linux"]
|
||||
},
|
||||
{
|
||||
name: "@electron-forge/maker-rpm",
|
||||
platforms: ["linux"]
|
||||
}
|
||||
],
|
||||
publishers: [
|
||||
{
|
||||
@@ -29,10 +96,51 @@ module.exports = {
|
||||
config: {
|
||||
repository: {
|
||||
owner: 'electron',
|
||||
name: 'fiddle'
|
||||
name: 'fiddle',
|
||||
},
|
||||
prerelease: true
|
||||
}
|
||||
}
|
||||
]
|
||||
draft: true,
|
||||
prerelease: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function notarizeMaybe() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.npm_lifecycle_event !== 'publish') {
|
||||
console.log(`Not in "publish" mode, skipping notarization`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.CI) {
|
||||
console.log(`Not in CI, skipping notarization`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.TRAVIS_TAG && !process.env.NOTARIZE_WITHOUT_TAG) {
|
||||
console.log(`Not a tag, not notarizing`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
|
||||
console.warn(
|
||||
'Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
config.packagerConfig.osxNotarize = {
|
||||
appBundleId: 'com.electron.fiddle',
|
||||
appleId: process.env.APPLE_ID,
|
||||
appleIdPassword: process.env.APPLE_ID_PASSWORD,
|
||||
ascProvider: 'LT94ZKYDCJ',
|
||||
};
|
||||
}
|
||||
|
||||
notarizeMaybe();
|
||||
|
||||
// Finally, export it
|
||||
module.exports = config;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"transform": {
|
||||
"\\.(js|jsx|ts|tsx)$": "ts-jest"
|
||||
},
|
||||
"testURL": "http://localhost",
|
||||
"testRegex": "(-spec)\\.(ts|tsx)$",
|
||||
"resetMocks": true,
|
||||
"bail": true,
|
||||
"resetModules": true,
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
],
|
||||
"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
+18986
-4024
File diff suppressed because it is too large
Load Diff
+112
-48
@@ -1,66 +1,130 @@
|
||||
{
|
||||
"name": "electron-fiddle",
|
||||
"productName": "electron-fiddle",
|
||||
"version": "1.0.0",
|
||||
"description": "My Electron application description",
|
||||
"productName": "Electron Fiddle",
|
||||
"version": "0.16.0",
|
||||
"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": "eslint \"./**/*.{js,ts,tsx}\" --fix",
|
||||
"lint:templates": "standard \"./static/show-me/**/*.js\" --fix",
|
||||
"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",
|
||||
"test:ci": "jest --config=jest.json --coverage --runInBand",
|
||||
"test:coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
|
||||
"test:report": "jest --config=jest.json --json --bail=false --outputFile=report.json | true",
|
||||
"tsc": "tsc --noEmit -p .",
|
||||
"electron-releases": "node --unhandled-rejections=strict ./tools/fetch-releases.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"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",
|
||||
"@octokit/rest": "^15.8.1",
|
||||
"builtin-modules": "^3.0.0",
|
||||
"electron-download": "^4.1.0",
|
||||
"@blueprintjs/core": "^3.24.0",
|
||||
"@blueprintjs/select": "^3.12.0",
|
||||
"@electron/get": "^1.9.0",
|
||||
"@octokit/rest": "^16.43.1",
|
||||
"@sentry/electron": "^1.2.1",
|
||||
"classnames": "^2.2.6",
|
||||
"electron-default-menu": "^1.0.2",
|
||||
"electron-devtools-installer": "^3.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": "^3.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"mobx": "^5.15.4",
|
||||
"mobx-react": "^6.1.8",
|
||||
"monaco-editor": "^0.20.0",
|
||||
"monaco-loader": "1.0.0",
|
||||
"namor": "^2.0.2",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-mosaic-component": "^3.2.0",
|
||||
"semver": "^7.1.3",
|
||||
"tmp": "0.1.0",
|
||||
"tslib": "^1.11.1",
|
||||
"update-electron-app": "^2.0.1"
|
||||
},
|
||||
"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.8.6",
|
||||
"@electron-forge/cli": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.52",
|
||||
"@electron-forge/publisher-github": "^6.0.0-beta.52",
|
||||
"@octokit/action": "^2.0.0",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/enzyme": "^3.10.5",
|
||||
"@types/fs-extra": "^8.1.0",
|
||||
"@types/jest": "^25.1.3",
|
||||
"@types/log-symbols": "^3.0.0",
|
||||
"@types/node": "^13.7.6",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/semver": "^7.1.0",
|
||||
"@types/tmp": "0.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^3.7.0",
|
||||
"@typescript-eslint/parser": "^3.7.0",
|
||||
"chokidar": "^3.3.1",
|
||||
"coveralls": "^3.0.9",
|
||||
"electron": "^9.3.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"enzyme-to-json": "^3.4.4",
|
||||
"eslint": "^7.5.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.20.3",
|
||||
"fetch-mock-jest": "^1.1.0-beta.3",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^25.1.0",
|
||||
"less": "^3.11.1",
|
||||
"lint-staged": "^10.2.11",
|
||||
"log-symbols": "^3.0.0",
|
||||
"node-abi": "^2.18.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^2.0.5",
|
||||
"react-test-renderer": "^16.13.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"standard": "^14.3.1",
|
||||
"stylelint": "^13.2.0",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.8.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/*.{js,ts,tsx}": [
|
||||
"eslint --fix"
|
||||
],
|
||||
"./static/show-me/**/*.js": [
|
||||
"standard --fix"
|
||||
],
|
||||
"./src/less/*.less": [
|
||||
"stylelint --fix"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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,8 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
export const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hello World!</title>
|
||||
<link rel="stylesheet" type="text/css" href="./styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World!</h1>
|
||||
@@ -16,4 +17,4 @@
|
||||
require('./renderer.js')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>`;
|
||||
@@ -1,27 +1,21 @@
|
||||
// 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
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow
|
||||
const path = require('path')
|
||||
const url = require('url')
|
||||
|
||||
function createWindow () {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({width: 800, height: 600})
|
||||
const mainWindow = new BrowserWindow({width: 800, height: 600})
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile('index.html')
|
||||
mainWindow.loadURL(url.format({
|
||||
protocol: 'file',
|
||||
slashes: true,
|
||||
pathname: path.resolve(app.getAppPath(), 'index.html')
|
||||
}))
|
||||
|
||||
// Open the DevTools.
|
||||
// mainWindow.webContents.openDevTools()
|
||||
|
||||
// Emitted when the window is closed.
|
||||
mainWindow.on('closed', function () {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
mainWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -41,10 +35,10 @@ app.on('window-all-closed', function () {
|
||||
app.on('activate', function () {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
// 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,44 @@
|
||||
export const main = `// Modules to control application life and create native browser window
|
||||
const {app, BrowserWindow} = require('electron')
|
||||
|
||||
function createWindow () {
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
})
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile('index.html')
|
||||
|
||||
// Open the DevTools.
|
||||
// mainWindow.webContents.openDevTools()
|
||||
}
|
||||
|
||||
// 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.whenReady().then(createWindow)
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
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 (BrowserWindow.getAllWindows().length === 0) {
|
||||
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 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.`;
|
||||
+111
-18
@@ -1,27 +1,120 @@
|
||||
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 VersionState {
|
||||
ready = 'ready',
|
||||
downloading = 'downloading',
|
||||
unzipping = 'unzipping',
|
||||
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 VersionSource {
|
||||
remote = 'remote',
|
||||
local = 'local',
|
||||
}
|
||||
|
||||
export interface ElectronVersion extends GitHubVersion {
|
||||
state: ElectronVersionState;
|
||||
export enum GistActionType {
|
||||
publish = 'Publish',
|
||||
update = 'Update',
|
||||
delete = 'Delete',
|
||||
}
|
||||
|
||||
export enum GistActionState {
|
||||
publishing = 'publishing',
|
||||
updating = 'updating',
|
||||
deleting = 'deleting',
|
||||
none = 'none',
|
||||
}
|
||||
|
||||
export interface Version {
|
||||
version: string;
|
||||
name?: string;
|
||||
localPath?: string;
|
||||
}
|
||||
|
||||
export interface EditorValues {
|
||||
main: string;
|
||||
renderer: string;
|
||||
html: string;
|
||||
preload: string;
|
||||
css: string;
|
||||
package?: string;
|
||||
}
|
||||
|
||||
export interface RunnableVersion extends Version {
|
||||
state: VersionState;
|
||||
source: VersionSource;
|
||||
downloadProgress?: number;
|
||||
}
|
||||
|
||||
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 GenericDialogOptions {
|
||||
type: GenericDialogType;
|
||||
ok?: string;
|
||||
cancel?: string;
|
||||
wantsInput?: boolean;
|
||||
label: string | JSX.Element;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface Templates {
|
||||
[index: string]: string | Templates;
|
||||
}
|
||||
|
||||
export const enum GenericDialogType {
|
||||
'confirm' = 'confirm',
|
||||
'warning' = 'warning',
|
||||
'success' = 'success',
|
||||
}
|
||||
|
||||
// Editors
|
||||
export const enum EditorId {
|
||||
'main' = 'main',
|
||||
'renderer' = 'renderer',
|
||||
'html' = 'html',
|
||||
'preload' = 'preload',
|
||||
'css' = 'css',
|
||||
}
|
||||
|
||||
// Panels that can show up as a mosaic
|
||||
export const enum PanelId {
|
||||
'docsDemo' = 'docsDemo',
|
||||
}
|
||||
|
||||
export type MosaicId = EditorId | PanelId;
|
||||
|
||||
export const ALL_EDITORS = [
|
||||
EditorId.main,
|
||||
EditorId.renderer,
|
||||
EditorId.preload,
|
||||
EditorId.html,
|
||||
EditorId.css,
|
||||
];
|
||||
export const ALL_PANELS = [PanelId.docsDemo];
|
||||
export const ALL_MOSAICS = [...ALL_EDITORS, ...ALL_PANELS];
|
||||
|
||||
export type ArrowPosition = 'top' | 'left' | 'bottom' | 'right';
|
||||
|
||||
export const enum DocsDemoPage {
|
||||
DEFAULT = 'DEFAULT',
|
||||
DEMO_APP = 'DEMO_APP',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
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',
|
||||
SHOW_INACTIVE = 'SHOW_INACTIVE',
|
||||
BEFORE_QUIT = 'BEFORE_QUIT',
|
||||
CONFIRM_QUIT = 'CONFIRM_QUIT',
|
||||
SET_APPDATA_DIR = 'SET_APPDATA_DIR',
|
||||
SELECT_ALL_IN_EDITOR = 'SELECT_ALL_IN_EDITOR',
|
||||
}
|
||||
|
||||
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,
|
||||
IpcEvents.SHOW_INACTIVE,
|
||||
IpcEvents.CONFIRM_QUIT,
|
||||
];
|
||||
|
||||
export const ipcRendererEvents = [
|
||||
IpcEvents.OPEN_SETTINGS,
|
||||
IpcEvents.LOAD_GIST_REQUEST,
|
||||
IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST,
|
||||
IpcEvents.FIDDLE_RUN,
|
||||
IpcEvents.FIDDLE_PACKAGE,
|
||||
IpcEvents.FIDDLE_MAKE,
|
||||
IpcEvents.MONACO_EXECUTE_COMMAND,
|
||||
IpcEvents.MONACO_TOGGLE_OPTION,
|
||||
IpcEvents.FS_NEW_FIDDLE,
|
||||
IpcEvents.FS_OPEN_FIDDLE,
|
||||
IpcEvents.FS_OPEN_TEMPLATE,
|
||||
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,
|
||||
IpcEvents.BEFORE_QUIT,
|
||||
IpcEvents.SET_APPDATA_DIR,
|
||||
IpcEvents.SELECT_ALL_IN_EDITOR,
|
||||
];
|
||||
|
||||
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,45 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update if list of Editor ID changes
|
||||
@editor-ids: main, renderer, html, preload;
|
||||
|
||||
// takes each editor ID and increase its z-index if the parent Mosiac root
|
||||
// has focused__id class for that specific id.
|
||||
each(@editor-ids, {
|
||||
.focused__@{value}.mosaic .mosaic-window.@{value} {
|
||||
z-index: 2;
|
||||
}
|
||||
});
|
||||
|
||||
.mosaic-window {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@@ -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,5 @@
|
||||
.bp3-fill {
|
||||
.version-chooser {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -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,20 @@
|
||||
// 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";
|
||||
@import "components/version-select.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;
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
const createWindow = () => {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 900,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
acceptFirstMouse: true
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile('./static/index.html');
|
||||
|
||||
// Open the DevTools.
|
||||
// mainWindow.webContents.openDevTools();
|
||||
|
||||
// Emitted when the window is closed.
|
||||
mainWindow.on('closed', () => {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
mainWindow = null;
|
||||
});
|
||||
};
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow);
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
|
||||
// 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.
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AboutPanelOptionsOptions, app } from 'electron';
|
||||
|
||||
/**
|
||||
* Sets Fiddle's About panel options on Linux and macOS
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export function setupAboutPanel(): void {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const options: AboutPanelOptionsOptions = {
|
||||
applicationName: 'Electron Fiddle',
|
||||
applicationVersion: app.getVersion(),
|
||||
version: process.versions.electron,
|
||||
copyright: '© Electron Authors',
|
||||
};
|
||||
|
||||
switch (process.platform) {
|
||||
case 'linux':
|
||||
options.website = 'https://electronjs.org/fiddle';
|
||||
case 'darwin':
|
||||
options.credits =
|
||||
'https://github.com/electron/fiddle/graphs/contributors';
|
||||
default:
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
app.setAboutPanelOptions(options);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
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,63 @@
|
||||
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,110 @@
|
||||
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,74 @@
|
||||
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.removeAllListeners(name);
|
||||
ipcMain.on(name, (...args: Array<any>) => this.emit(name, ...args));
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
WEBCONTENTS_READY_FOR_IPC_SIGNAL,
|
||||
(event: Electron.IpcMainEvent) => {
|
||||
this.readyWebContents.add(event.sender);
|
||||
|
||||
const queue = this.messageQueue.get(event.sender);
|
||||
this.messageQueue.delete(event.sender);
|
||||
if (!queue) return;
|
||||
for (const item of queue) {
|
||||
this.send(item[0], item[1], event.sender);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,98 @@
|
||||
import { initSentry } from '../sentry';
|
||||
initSentry();
|
||||
|
||||
import { app } from 'electron';
|
||||
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
import { setupAboutPanel } from './about-panel';
|
||||
import { setupDevTools } from './devtools';
|
||||
import { setupDialogs } from './dialogs';
|
||||
import { onFirstRunMaybe } from './first-run';
|
||||
import { ipcMainManager } from './ipc';
|
||||
import { listenForProtocolHandler, setupProtocolHandler } from './protocol';
|
||||
import { shouldQuit } from './squirrel';
|
||||
import { setupUpdates } from './update';
|
||||
import { getOrCreateMainWindow } from './windows';
|
||||
|
||||
/**
|
||||
* Handle 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() {
|
||||
ipcMainManager.send(IpcEvents.BEFORE_QUIT);
|
||||
ipcMainManager.on(IpcEvents.CONFIRM_QUIT, quitAppIfConfirmed);
|
||||
}
|
||||
|
||||
export function quitAppIfConfirmed(
|
||||
_: Electron.IpcMainEvent,
|
||||
quitConfirmed: boolean,
|
||||
) {
|
||||
if (quitConfirmed) {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All windows have been closed, quit on anything but
|
||||
* macOS.
|
||||
*/
|
||||
export function onWindowsAllClosed() {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.whenReady().then(onReady);
|
||||
app.on('before-quit', onBeforeQuit);
|
||||
app.on('window-all-closed', onWindowsAllClosed);
|
||||
app.on('activate', getOrCreateMainWindow);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,337 @@
|
||||
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.toggleDevTools();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
{
|
||||
role: '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.name &&
|
||||
isSubmenu(item.submenu)
|
||||
) {
|
||||
item.submenu.splice(2, 0, ...getPreferencesItems());
|
||||
}
|
||||
|
||||
// Custom handler for "Select All" for Monaco
|
||||
if (label === 'Edit' && isSubmenu(item.submenu)) {
|
||||
const selectAll = item.submenu.find((i) => i.label === 'Select All')!;
|
||||
delete selectAll.role; // override default role
|
||||
selectAll.click = () => {
|
||||
ipcMainManager.send(IpcEvents.SELECT_ALL_IN_EDITOR);
|
||||
|
||||
// Allow selection to occur in text fields outside the editors.
|
||||
if (process.platform === 'darwin') {
|
||||
Menu.sendActionToFirstResponder('selectAll:');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Tweak "View" menu
|
||||
if (label === 'View' && isSubmenu(item.submenu)) {
|
||||
// remove "Reload" (has weird behaviour) and "Toggle Developer Tools"
|
||||
item.submenu = item.submenu.filter(
|
||||
(subItem) =>
|
||||
subItem.label !== 'Toggle Developer Tools' &&
|
||||
subItem.label !== 'Reload',
|
||||
);
|
||||
item.submenu.push(
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
); // Add zooming actions
|
||||
item.submenu.push(
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Toggle Soft Wrap',
|
||||
click: () =>
|
||||
ipcMainManager.send(IpcEvents.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,109 @@
|
||||
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.removeAllListeners('open-url');
|
||||
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,94 @@
|
||||
import { app, BrowserWindow, shell } from 'electron';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { createContextMenu } from './context-menu';
|
||||
import { ipcMainManager } from './ipc';
|
||||
|
||||
// Keep a global reference of the window objects, if we don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
export let browserWindows: Array<BrowserWindow | null> = [];
|
||||
|
||||
/**
|
||||
* 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`);
|
||||
let browserWindow: BrowserWindow | null;
|
||||
browserWindow = new BrowserWindow(getMainWindowOptions());
|
||||
browserWindow.loadFile('./dist/static/index.html');
|
||||
|
||||
browserWindow.webContents.once('dom-ready', () => {
|
||||
if (browserWindow) {
|
||||
browserWindow.show();
|
||||
|
||||
createContextMenu(browserWindow);
|
||||
}
|
||||
});
|
||||
|
||||
browserWindow.on('closed', () => {
|
||||
browserWindows = browserWindows.filter((bw) => browserWindow !== bw);
|
||||
|
||||
browserWindow = null;
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('new-window', (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
ipcMainManager.on(IpcEvents.SHOW_INACTIVE, () => {
|
||||
if (browserWindow) {
|
||||
browserWindow.showInactive();
|
||||
}
|
||||
});
|
||||
|
||||
const appData = app.getPath('appData');
|
||||
ipcMainManager.send(
|
||||
IpcEvents.SET_APPDATA_DIR,
|
||||
[appData],
|
||||
browserWindow.webContents,
|
||||
);
|
||||
|
||||
browserWindows.push(browserWindow);
|
||||
|
||||
return browserWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
);
|
||||
}
|
||||
+214
-81
@@ -1,96 +1,229 @@
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import * as loader from 'monaco-loader';
|
||||
import { observable } from 'mobx';
|
||||
import { initSentry } from '../sentry';
|
||||
initSentry();
|
||||
|
||||
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 { when } from 'mobx';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
|
||||
const knownVersions = getKnownVersions();
|
||||
const defaultVersion = normalizeVersion(knownVersions[0].tag_name);
|
||||
import { ipcRenderer } from 'electron';
|
||||
import {
|
||||
ALL_EDITORS,
|
||||
EditorId,
|
||||
EditorValues,
|
||||
GenericDialogType,
|
||||
SetFiddleOptions,
|
||||
} from '../interfaces';
|
||||
import { WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
|
||||
import { updateEditorLayout } from '../utils/editor-layout';
|
||||
import { getEditorValue } from '../utils/editor-value';
|
||||
import { getPackageJson, PackageJsonOptions } from '../utils/get-package';
|
||||
import { getTitle } from '../utils/get-title';
|
||||
import { isEditorBackup } from '../utils/type-checks';
|
||||
import { EMPTY_EDITOR_CONTENT } from './constants';
|
||||
import { FileManager } from './file-manager';
|
||||
import { RemoteLoader } from './remote-loader';
|
||||
import { Runner } from './runner';
|
||||
import { appState } from './state';
|
||||
import { getTheme } from './themes';
|
||||
import { TouchBarManager } from './touch-bar-manager';
|
||||
|
||||
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.setGenericDialogOptions({
|
||||
type: GenericDialogType.warning,
|
||||
label: `Opening this Fiddle will replace your unsaved changes. Do you want to proceed?`,
|
||||
ok: 'Yes',
|
||||
});
|
||||
this.state.isGenericDialogShowing = true;
|
||||
await when(() => !this.state.isGenericDialogShowing);
|
||||
|
||||
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.genericDialogLastResult) {
|
||||
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'
|
||||
})
|
||||
// display all editors that have content
|
||||
const visibleEditors = [];
|
||||
for (const id of Object.keys(editorValues)) {
|
||||
const content = editorValues[id];
|
||||
|
||||
// if the gist content matches the empty file output, don't show it
|
||||
if (!Object.values(EMPTY_EDITOR_CONTENT).includes(content)) {
|
||||
visibleEditors.push(id as EditorId);
|
||||
}
|
||||
}
|
||||
|
||||
this.state.gistId = gistId || '';
|
||||
this.state.localPath = filePath;
|
||||
this.state.templateName = templateName;
|
||||
|
||||
// once loaded, we have a "saved" state
|
||||
await this.state.setVisibleMosaics(visibleEditors);
|
||||
await this.setEditorValues(editorValues);
|
||||
this.state.isUnsaved = false;
|
||||
|
||||
document.title = getTitle(this.state);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the contents of all editor panes.
|
||||
*
|
||||
* @param {EditorValues} values
|
||||
*/
|
||||
public async setEditorValues(values: Partial<EditorValues>): Promise<void> {
|
||||
const { ElectronFiddle: fiddle } = window;
|
||||
|
||||
if (!fiddle) {
|
||||
throw new Error('Fiddle not ready');
|
||||
}
|
||||
|
||||
for (const name of ALL_EDITORS) {
|
||||
const editor = fiddle.editors[name];
|
||||
const backup = this.state.closedPanels[name];
|
||||
|
||||
if (typeof values[name] !== 'undefined') {
|
||||
if (isEditorBackup(backup)) {
|
||||
// The editor does not exist, attempt to set it on the backup.
|
||||
// If there's a model, we'll do it on the model. Else, we'll
|
||||
// set the value.
|
||||
|
||||
if (backup.model) {
|
||||
backup.model.setValue(values[name]!);
|
||||
} else {
|
||||
backup.value = values[name]!;
|
||||
}
|
||||
} else if (editor && editor.setValue) {
|
||||
// The editor exists, set the value directly
|
||||
editor.setValue(values[name]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contents of all editor panes.
|
||||
*
|
||||
* @returns {EditorValues}
|
||||
*/
|
||||
public async getEditorValues(
|
||||
options?: PackageJsonOptions,
|
||||
): Promise<EditorValues> {
|
||||
const { ElectronFiddle: fiddle } = window;
|
||||
|
||||
if (!fiddle) {
|
||||
throw new Error('Fiddle not ready');
|
||||
}
|
||||
|
||||
const values: EditorValues = {
|
||||
css: getEditorValue(EditorId.css),
|
||||
html: getEditorValue(EditorId.html),
|
||||
main: getEditorValue(EditorId.main),
|
||||
preload: getEditorValue(EditorId.preload),
|
||||
renderer: getEditorValue(EditorId.renderer),
|
||||
};
|
||||
|
||||
if (options && options.include !== false) {
|
||||
values.package = await getPackageJson(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);
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
+251
-91
@@ -1,115 +1,275 @@
|
||||
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 { VersionState } from '../interfaces';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { normalizeVersion } from '../utils/normalize-version';
|
||||
import { StringMap } from '../interfaces';
|
||||
import { USER_DATA_PATH } from './constants';
|
||||
import { removeTypeDefsForVersion } from './fetch-types';
|
||||
import { AppState } from './state';
|
||||
|
||||
const eDownload = promisify(require('electron-download'));
|
||||
/**
|
||||
* 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>}
|
||||
*/
|
||||
export async function setupBinary(
|
||||
appState: AppState,
|
||||
iVersion: string,
|
||||
): Promise<void> {
|
||||
const version = normalizeVersion(iVersion);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
|
||||
export class BinaryManager extends EventEmitter {
|
||||
public state: StringMap<'ready' | 'downloading'> = {};
|
||||
public version: string = '2.0.2';
|
||||
await fs.mkdirp(getDownloadPath(version));
|
||||
|
||||
constructor(version: string) {
|
||||
super();
|
||||
|
||||
this.setup(version);
|
||||
const { state } = appState.versions[version];
|
||||
if (state === VersionState.downloading || state === VersionState.unzipping) {
|
||||
console.log(`Binary: Electron ${version} already downloading.`);
|
||||
return;
|
||||
}
|
||||
|
||||
public async setup(iVersion: string) {
|
||||
const version = normalizeVersion(iVersion);
|
||||
await fs.mkdirp(this.getDownloadPath(version));
|
||||
|
||||
if (this.state[version] === 'downloading') {
|
||||
console.log(`BinaryManager: Electron ${version} already downloading.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getIsDownloaded(version)) {
|
||||
console.log(`BinaryManager: Electron ${version} already downloaded.`);
|
||||
this.state[version] = 'ready';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`BinaryManager: Electron ${version} not present, downloading`);
|
||||
this.state[version] = 'downloading';
|
||||
|
||||
const zipPath = await eDownload({ version });
|
||||
const extractPath = this.getDownloadPath(version);
|
||||
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking`);
|
||||
const electronFiles = await this.unzip(zipPath, extractPath);
|
||||
console.log(electronFiles);
|
||||
|
||||
this.state[version] = 'ready';
|
||||
if (await getIsDownloaded(version)) {
|
||||
console.log(`Binary: Electron ${version} already downloaded.`);
|
||||
appState.versions[version].state = VersionState.ready;
|
||||
return;
|
||||
}
|
||||
|
||||
public getElectronBinary(version: string) {
|
||||
const platform = os.platform();
|
||||
const dir = this.getDownloadPath(version);
|
||||
console.log(`Binary: Electron ${version} not present, downloading`);
|
||||
appState.versions[version].state = VersionState.downloading;
|
||||
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
return path.join(dir, 'Electron.app/Contents/MacOS/Electron');
|
||||
case 'freebsd':
|
||||
case 'linux':
|
||||
return path.join(dir, 'electron');
|
||||
case 'win32':
|
||||
return path.join(dir, 'electron.exe');
|
||||
default:
|
||||
throw new Error('Electron builds are not available on platform: ' + platform);
|
||||
}
|
||||
const zipPath = await download(appState, version);
|
||||
const extractPath = getDownloadPath(version);
|
||||
console.log(
|
||||
`Binary: Electron ${version} downloaded, now unpacking to ${extractPath}`,
|
||||
);
|
||||
|
||||
try {
|
||||
appState.versions[version].state = VersionState.unzipping;
|
||||
|
||||
// Ensure the target path is empty
|
||||
await fs.emptyDir(extractPath);
|
||||
|
||||
const electronFiles = await unzip(zipPath, extractPath);
|
||||
console.log(`Unzipped ${version}`, electronFiles);
|
||||
} catch (error) {
|
||||
console.warn(`Failure while unzipping ${version}`, error);
|
||||
appState.versions[version].state = VersionState.unknown;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public async getDownloadedVersions(): Promise<Array<string>> {
|
||||
const downloadPath = path.join(userData, 'electron-bin');
|
||||
console.log(`BinaryManager: Checking for downloaded versions`);
|
||||
appState.versions[version].state = VersionState.ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a version from disk. Does not update state. We'll try up to
|
||||
* four times before giving up if an error occurs.
|
||||
*
|
||||
* @param {string} iVersion
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function removeBinary(iVersion: string) {
|
||||
const version = normalizeVersion(iVersion);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
let isDeleted = false;
|
||||
|
||||
// utility to re-run removal functions upon failure
|
||||
// due to windows filesystem lockfile jank
|
||||
const rerunner = async (func: () => Promise<void>, counter = 1) => {
|
||||
try {
|
||||
const directories = await fs.readdir(downloadPath);
|
||||
const knownVersions: Array<string> = [];
|
||||
|
||||
for (const directory of directories) {
|
||||
if (this.getIsDownloaded(directory)) {
|
||||
knownVersions.push(directory);
|
||||
}
|
||||
}
|
||||
|
||||
return knownVersions;
|
||||
await func();
|
||||
} catch (error) {
|
||||
console.warn(`Could not read known Electron versions`);
|
||||
return [];
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public getIsDownloaded(version: string) {
|
||||
const expectedPath = this.getElectronBinary(version);
|
||||
return fs.existsSync(expectedPath);
|
||||
}
|
||||
|
||||
private getDownloadPath(version: string) {
|
||||
return path.join(userData, 'electron-bin', version);
|
||||
}
|
||||
|
||||
private unzip(zipPath: string, extractPath: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const binaryCleaner = async () => {
|
||||
if (await getIsDownloaded(version)) {
|
||||
// This is necessary since we're messing with .asar files inside
|
||||
// the Electron binaries. Electron, powering Fiddle, will try to
|
||||
// "correct" our calls, but we don't want that right here.
|
||||
process.noAsar = true;
|
||||
await fs.remove(getDownloadPath(version));
|
||||
process.noAsar = false;
|
||||
|
||||
extract(zipPath, { dir: extractPath }, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
isDeleted = true;
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`BinaryManager: Unpacked!`);
|
||||
process.noAsar = false;
|
||||
const typeDefsCleaner = async () => {
|
||||
await removeTypeDefsForVersion(version);
|
||||
};
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await rerunner(binaryCleaner);
|
||||
|
||||
if (isDeleted) {
|
||||
await rerunner(typeDefsCleaner);
|
||||
}
|
||||
}
|
||||
|
||||
/* Did we already download a given version?
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} dir
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export async function getIsDownloaded(
|
||||
version: string,
|
||||
dir?: string,
|
||||
): Promise<boolean> {
|
||||
const expectedPath = getElectronBinaryPath(version, dir);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
|
||||
return fs.existsSync(expectedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expected path for the binary of a given Electron version
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} dir
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getElectronBinaryPath(
|
||||
version: string,
|
||||
dir: string = getDownloadPath(version),
|
||||
): string {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return path.join(dir, 'Electron.app/Contents/MacOS/Electron');
|
||||
case 'freebsd':
|
||||
case 'linux':
|
||||
return path.join(dir, 'electron');
|
||||
case 'win32':
|
||||
return path.join(dir, 'electron.exe');
|
||||
default:
|
||||
throw new Error(
|
||||
`Electron builds are not available for ${process.platform}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getDownloadingVersions(appState: AppState) {
|
||||
return Object.entries(appState.versions)
|
||||
.filter(([_, { state }]) => state === 'downloading')
|
||||
.map(([version, _]) => version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all versions downloaded to disk
|
||||
*
|
||||
* @returns {Promise<Array<string>>}
|
||||
*/
|
||||
export async function getDownloadedVersions(): Promise<Array<string>> {
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
const downloadPath = path.join(USER_DATA_PATH, 'electron-bin');
|
||||
console.log(`Binary: Checking for downloaded versions`);
|
||||
|
||||
try {
|
||||
const directories = await fs.readdir(downloadPath);
|
||||
const knownVersions: Array<string> = [];
|
||||
|
||||
for (const directory of directories) {
|
||||
if (await getIsDownloaded(directory)) {
|
||||
knownVersions.push(directory);
|
||||
}
|
||||
}
|
||||
|
||||
return knownVersions;
|
||||
} catch (error) {
|
||||
console.warn(`Could not read known Electron versions`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an Electron version.
|
||||
*
|
||||
* @param {AppState} appState
|
||||
* @param {string} version
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function download(appState: AppState, version: string): Promise<string> {
|
||||
const { download: electronDownload } = await import('@electron/get');
|
||||
const getProgressCallback = (progress: Progress) => {
|
||||
const roundedProgress = Math.round(progress.percent * 100) / 100;
|
||||
|
||||
if (roundedProgress !== appState.versions[version].downloadProgress) {
|
||||
console.debug(
|
||||
`Binary: Version ${version} download progress: ${progress.percent}`,
|
||||
);
|
||||
appState.versions[version].downloadProgress = roundedProgress;
|
||||
}
|
||||
};
|
||||
|
||||
if (!appState.versions[version]) {
|
||||
throw new Error(
|
||||
`Version ${version} does not exist in state, cannot download`,
|
||||
);
|
||||
}
|
||||
|
||||
const zipFilePath = await electronDownload(version, {
|
||||
downloadOptions: {
|
||||
quiet: true,
|
||||
getProgressCallback,
|
||||
},
|
||||
});
|
||||
|
||||
return zipFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expected path for a given Electron version
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
function getDownloadPath(version: string): string {
|
||||
return path.join(USER_DATA_PATH, 'electron-bin', version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzips an electron package so that we can actually use it.
|
||||
*
|
||||
* @param {string} zipPath
|
||||
* @param {string} extractPath
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function unzip(zipPath: string, extractPath: string): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const extract = (await fancyImport<any>('extract-zip')).default;
|
||||
|
||||
process.noAsar = true;
|
||||
|
||||
const options = {
|
||||
dir: extractPath,
|
||||
};
|
||||
|
||||
extract(zipPath, options, (error: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Binary: Unpacked!`);
|
||||
process.noAsar = false;
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface Progress {
|
||||
percent: number;
|
||||
transferred: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { RunnableVersion } from '../interfaces';
|
||||
|
||||
export class Bisector {
|
||||
public revList: Array<RunnableVersion>;
|
||||
public minRev: number;
|
||||
public maxRev: number;
|
||||
private pivot: number;
|
||||
|
||||
constructor(revList: Array<RunnableVersion>) {
|
||||
this.getCurrentVersion = this.getCurrentVersion.bind(this);
|
||||
this.continue = this.continue.bind(this);
|
||||
this.calculatePivot = this.calculatePivot.bind(this);
|
||||
|
||||
this.revList = revList;
|
||||
this.minRev = 0;
|
||||
this.maxRev = revList.length - 1;
|
||||
this.calculatePivot();
|
||||
}
|
||||
|
||||
public getCurrentVersion() {
|
||||
return this.revList[this.pivot];
|
||||
}
|
||||
|
||||
public continue(isGoodVersion: boolean) {
|
||||
let isBisectOver = false;
|
||||
if (this.maxRev - this.minRev <= 1) {
|
||||
isBisectOver = true;
|
||||
}
|
||||
|
||||
if (isGoodVersion) {
|
||||
const upPivot = Math.floor((this.maxRev - this.pivot) / 2) + this.pivot;
|
||||
this.minRev = this.pivot;
|
||||
if (upPivot !== this.maxRev && upPivot !== this.pivot) {
|
||||
this.pivot = upPivot;
|
||||
} else {
|
||||
isBisectOver = true;
|
||||
}
|
||||
} else {
|
||||
const downPivot =
|
||||
Math.floor((this.pivot - this.minRev) / 2) + this.minRev;
|
||||
this.maxRev = this.pivot;
|
||||
if (downPivot !== this.minRev && downPivot !== this.pivot) {
|
||||
this.pivot = downPivot;
|
||||
} else {
|
||||
isBisectOver = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isBisectOver) {
|
||||
return [this.revList[this.minRev], this.revList[this.maxRev]];
|
||||
} else {
|
||||
return this.revList[this.pivot];
|
||||
}
|
||||
}
|
||||
|
||||
private calculatePivot() {
|
||||
this.pivot = Math.floor((this.maxRev - this.minRev) / 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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,440 @@
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
IToastProps,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Position,
|
||||
Toaster,
|
||||
} from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { when } from 'mobx';
|
||||
import {
|
||||
EditorValues,
|
||||
GenericDialogType,
|
||||
GistActionState,
|
||||
GistActionType,
|
||||
} from '../../interfaces';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import {
|
||||
INDEX_HTML_NAME,
|
||||
MAIN_JS_NAME,
|
||||
PRELOAD_JS_NAME,
|
||||
RENDERER_JS_NAME,
|
||||
STYLES_CSS_NAME,
|
||||
} from '../../shared-constants';
|
||||
import { getOctokit } from '../../utils/octokit';
|
||||
import { EMPTY_EDITOR_CONTENT } from '../constants';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface GistActionButtonProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
interface IGistActionButtonState {
|
||||
readonly isUpdating: boolean;
|
||||
readonly isDeleting: boolean;
|
||||
readonly actionType: GistActionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "publish" button takes care of logging you in.
|
||||
*
|
||||
* @export
|
||||
* @class GistActionButton
|
||||
* @extends {React.Component<GistActionButtonProps, GistActionButtonState>}
|
||||
*/
|
||||
@observer
|
||||
export class GistActionButton extends React.Component<
|
||||
GistActionButtonProps,
|
||||
IGistActionButtonState
|
||||
> {
|
||||
public constructor(props: GistActionButtonProps) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.performGistAction = this.performGistAction.bind(this);
|
||||
this.setPrivate = this.setPrivate.bind(this);
|
||||
this.setPublic = this.setPublic.bind(this);
|
||||
|
||||
this.state = {
|
||||
isUpdating: false,
|
||||
isDeleting: false,
|
||||
actionType: GistActionType.publish,
|
||||
};
|
||||
}
|
||||
|
||||
private toaster: Toaster;
|
||||
private refHandlers = {
|
||||
toaster: (ref: Toaster) => (this.toaster = ref),
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
ipcRendererManager.off(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user clicks the publish button, we either show the
|
||||
* authentication dialog or publish right away.
|
||||
*
|
||||
* 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 GistActionButton
|
||||
*/
|
||||
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.performGistAction();
|
||||
}
|
||||
}
|
||||
|
||||
public async getFiddleDescriptionFromUser(): Promise<string> {
|
||||
const { appState } = this.props;
|
||||
|
||||
// Reset potentially non-null last description.
|
||||
appState.genericDialogLastInput = null;
|
||||
|
||||
appState.setGenericDialogOptions({
|
||||
type: GenericDialogType.confirm,
|
||||
label: 'Please provide a brief description for your Fiddle Gist',
|
||||
wantsInput: true,
|
||||
ok: 'Publish',
|
||||
cancel: undefined,
|
||||
placeholder: 'Electron Fiddle Gist',
|
||||
});
|
||||
|
||||
appState.isGenericDialogShowing = true;
|
||||
await when(() => !appState.isGenericDialogShowing);
|
||||
|
||||
return appState.genericDialogLastInput ?? 'Electron Fiddle Gist';
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a new GitHub gist.
|
||||
*/
|
||||
public async handlePublish() {
|
||||
const { appState } = this.props;
|
||||
|
||||
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);
|
||||
|
||||
appState.activeGistAction = GistActionState.publishing;
|
||||
|
||||
try {
|
||||
const description = await this.getFiddleDescriptionFromUser();
|
||||
|
||||
const gist = await octo.gists.create({
|
||||
public: !!gitHubPublishAsPublic,
|
||||
description,
|
||||
files: this.gistFilesList(values) as any, // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
|
||||
});
|
||||
|
||||
appState.gistId = gist.data.id;
|
||||
appState.localPath = undefined;
|
||||
|
||||
console.log(`Publish Button: Publishing complete`, { gist });
|
||||
this.renderToast({ message: 'Publishing completed successfully!' });
|
||||
|
||||
// Only set action type to update if publish completed successfully.
|
||||
this.setActionType(GistActionType.update);
|
||||
} 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);
|
||||
}
|
||||
|
||||
// Ensure any previous input is reset.
|
||||
appState.genericDialogLastInput = null;
|
||||
appState.activeGistAction = GistActionState.none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing GitHub gist.
|
||||
*/
|
||||
public async handleUpdate() {
|
||||
const { appState } = this.props;
|
||||
const octo = await getOctokit(this.props.appState);
|
||||
const options = { includeDependencies: true, includeElectron: true };
|
||||
const values = await window.ElectronFiddle.app.getEditorValues(options);
|
||||
|
||||
appState.activeGistAction = GistActionState.updating;
|
||||
|
||||
try {
|
||||
const gist = await octo.gists.update({
|
||||
gist_id: appState.gistId!,
|
||||
files: this.gistFilesList(values) as any,
|
||||
});
|
||||
|
||||
console.log('Updating: Updating done', { gist });
|
||||
this.renderToast({ message: 'Successfully updated gist!' });
|
||||
} catch (error) {
|
||||
console.warn(`Could not update gist`, { error });
|
||||
|
||||
const messageBoxOptions: Electron.MessageBoxOptions = {
|
||||
message:
|
||||
'Updating Fiddle Gist failed. Are you connected to the Internet and is this your Gist?',
|
||||
detail: `GitHub encountered the following error: ${error.message}`,
|
||||
};
|
||||
|
||||
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
|
||||
}
|
||||
|
||||
appState.activeGistAction = GistActionState.none;
|
||||
this.setActionType(GistActionType.update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing GitHub gist.
|
||||
*/
|
||||
public async handleDelete() {
|
||||
const { appState } = this.props;
|
||||
const octo = await getOctokit(this.props.appState);
|
||||
|
||||
appState.activeGistAction = GistActionState.deleting;
|
||||
|
||||
try {
|
||||
const gist = await octo.gists.delete({
|
||||
gist_id: appState.gistId!,
|
||||
});
|
||||
|
||||
console.log('Deleting: Deleting done', { gist });
|
||||
this.renderToast({ message: 'Successfully deleted gist!' });
|
||||
} catch (error) {
|
||||
console.warn(`Could not delete gist`, { error });
|
||||
|
||||
const messageBoxOptions: Electron.MessageBoxOptions = {
|
||||
message:
|
||||
'Deleting Fiddle Gist failed. Are you connected to the Internet, is this your Gist, and have you loaded it?',
|
||||
detail: `GitHub encountered the following error: ${error.message}`,
|
||||
};
|
||||
|
||||
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
|
||||
}
|
||||
|
||||
appState.gistId = undefined;
|
||||
appState.activeGistAction = GistActionState.none;
|
||||
this.setActionType(GistActionType.publish);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect with GitHub, perform a publish/update/delete action,
|
||||
* and update all related properties in the app state.
|
||||
*/
|
||||
public async performGistAction(): Promise<void> {
|
||||
const { gistId } = this.props.appState;
|
||||
const { actionType } = this.state;
|
||||
|
||||
if (gistId) {
|
||||
switch (actionType) {
|
||||
case GistActionType.publish:
|
||||
await this.handlePublish();
|
||||
break;
|
||||
case GistActionType.update:
|
||||
await this.handleUpdate();
|
||||
break;
|
||||
case GistActionType.delete:
|
||||
await this.handleDelete();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
await this.handlePublish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish fiddles as private.
|
||||
*
|
||||
* @memberof GistActionButton
|
||||
*/
|
||||
public setPrivate() {
|
||||
this.setPrivacy(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish fiddles as public.
|
||||
*
|
||||
* @memberof GistActionButton
|
||||
*/
|
||||
public setPublic() {
|
||||
this.setPrivacy(true);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { gistId, activeGistAction } = this.props.appState;
|
||||
const { actionType } = this.state;
|
||||
|
||||
const getTextForButton = () => {
|
||||
let text;
|
||||
if (gistId) {
|
||||
text = actionType;
|
||||
} else if (activeGistAction === GistActionState.updating) {
|
||||
text = 'Updating...';
|
||||
} else if (activeGistAction === GistActionState.publishing) {
|
||||
text = 'Publishing...';
|
||||
} else if (activeGistAction === GistActionState.deleting) {
|
||||
text = 'Deleting...';
|
||||
} else {
|
||||
text = 'Publish';
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const getActionIcon = () => {
|
||||
switch (actionType) {
|
||||
case GistActionType.publish:
|
||||
return 'upload';
|
||||
case GistActionType.update:
|
||||
return 'refresh';
|
||||
case GistActionType.delete:
|
||||
return 'delete';
|
||||
}
|
||||
};
|
||||
|
||||
const isPerformingAction = activeGistAction !== GistActionState.none;
|
||||
return (
|
||||
<>
|
||||
<fieldset disabled={isPerformingAction}>
|
||||
<ButtonGroup className="button-gist-action">
|
||||
{this.renderPrivacyMenu()}
|
||||
<Button
|
||||
onClick={this.handleClick}
|
||||
loading={isPerformingAction}
|
||||
icon={getActionIcon()}
|
||||
text={getTextForButton()}
|
||||
/>
|
||||
{this.renderGistActionMenu()}
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
<Toaster
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
ref={this.refHandlers.toaster}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderGistActionMenu = () => {
|
||||
const { gistId } = this.props.appState;
|
||||
const { actionType } = this.state;
|
||||
|
||||
if (!gistId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text="Publish"
|
||||
active={actionType === GistActionType.publish}
|
||||
onClick={() => this.setActionType(GistActionType.publish)}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Update"
|
||||
active={actionType === GistActionType.update}
|
||||
onClick={() => this.setActionType(GistActionType.update)}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Delete"
|
||||
active={actionType === GistActionType.delete}
|
||||
onClick={() => this.setActionType(GistActionType.delete)}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={menu} position={Position.BOTTOM}>
|
||||
<Button icon="wrench" />
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
private renderPrivacyMenu = () => {
|
||||
const { gitHubPublishAsPublic, gistId } = this.props.appState;
|
||||
|
||||
if (gistId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 (
|
||||
<Popover content={privacyMenu} position={Position.BOTTOM}>
|
||||
<Button icon={privacyIcon} />
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
private setActionType = (actionType: GistActionType) => {
|
||||
this.setState({ actionType });
|
||||
};
|
||||
|
||||
private setPrivacy(publishAsPublic: boolean) {
|
||||
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
|
||||
}
|
||||
|
||||
private renderToast = (toast: IToastProps) => {
|
||||
this.toaster.show(toast);
|
||||
};
|
||||
|
||||
private gistFilesList = (values: EditorValues) => {
|
||||
return {
|
||||
[INDEX_HTML_NAME]: {
|
||||
content: values.html || EMPTY_EDITOR_CONTENT.html,
|
||||
},
|
||||
[MAIN_JS_NAME]: {
|
||||
content: values.main || EMPTY_EDITOR_CONTENT.js,
|
||||
},
|
||||
[RENDERER_JS_NAME]: {
|
||||
content: values.renderer || EMPTY_EDITOR_CONTENT.js,
|
||||
},
|
||||
[PRELOAD_JS_NAME]: {
|
||||
content: values.preload || EMPTY_EDITOR_CONTENT.js,
|
||||
},
|
||||
[STYLES_CSS_NAME]: {
|
||||
content: values.css || EMPTY_EDITOR_CONTENT.css,
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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 { GistActionState } from '../../interfaces';
|
||||
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 = 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, activeGistAction } = this.props.appState;
|
||||
const { value } = this.state;
|
||||
const isCorrect = /https:\/\/gist\.github\.com\/(.+)$/.test(value);
|
||||
const className = classnames('address-bar', isUnsaved, { empty: !value });
|
||||
|
||||
const isPerformingAction = activeGistAction !== GistActionState.none;
|
||||
return (
|
||||
<form className={className} onSubmit={this.handleSubmit}>
|
||||
<fieldset disabled={isPerformingAction}>
|
||||
<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,95 @@
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GenericDialogType, VersionState } from '../../../src/interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
interface BisectHandlerProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class BisectHandler extends React.Component<BisectHandlerProps> {
|
||||
constructor(props: BisectHandlerProps) {
|
||||
super(props);
|
||||
|
||||
this.continueBisect = this.continueBisect.bind(this);
|
||||
this.terminateBisect = this.terminateBisect.bind(this);
|
||||
}
|
||||
|
||||
public continueBisect(isGood: boolean) {
|
||||
window.ElectronFiddle.app.runner.stop();
|
||||
|
||||
const { appState } = this.props;
|
||||
const response = appState.Bisector!.continue(isGood);
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
this.terminateBisect();
|
||||
|
||||
const [minRev, maxRev] = response;
|
||||
const [minVer, maxVer] = [minRev.version, maxRev.version];
|
||||
const label = (
|
||||
<>
|
||||
Bisect complete. Check the range{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href={`https://github.com/electron/electron/compare/v${minVer}...v${maxVer}`}
|
||||
>
|
||||
{minVer}...{maxVer}
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
);
|
||||
|
||||
appState.pushOutput(`[BISECT] Complete: ${minVer}...${maxVer}`);
|
||||
appState.setGenericDialogOptions({
|
||||
type: GenericDialogType.success,
|
||||
label,
|
||||
cancel: undefined,
|
||||
});
|
||||
appState.isGenericDialogShowing = true;
|
||||
} else {
|
||||
appState.setVersion(response.version);
|
||||
}
|
||||
}
|
||||
|
||||
public terminateBisect() {
|
||||
const { appState } = this.props;
|
||||
appState.Bisector = undefined;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
if (!!appState.Bisector) {
|
||||
const isDownloading =
|
||||
appState.currentElectronVersion.state === VersionState.downloading;
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon={'thumbs-up'}
|
||||
onClick={() => this.continueBisect(true)}
|
||||
disabled={isDownloading}
|
||||
/>
|
||||
<Button
|
||||
icon={'thumbs-down'}
|
||||
onClick={() => this.continueBisect(false)}
|
||||
disabled={isDownloading}
|
||||
/>
|
||||
<Button icon={'cross'} onClick={this.terminateBisect}>
|
||||
Cancel Bisect
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
icon="git-branch"
|
||||
text="Bisect"
|
||||
onClick={appState.toggleBisectDialog}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
Button,
|
||||
Menu,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ALL_EDITORS, MosaicId, PanelId } from '../../interfaces';
|
||||
import { getVisibleMosaics } from '../../utils/editors-mosaic-arrangement';
|
||||
import { AppState } from '../state';
|
||||
import { TITLE_MAP } from './editors';
|
||||
|
||||
export interface EditorDropdownState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EditorDropdownProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dropdown allowing users to toggle the various editors
|
||||
*
|
||||
* @class EditorDropdown
|
||||
* @extends {React.Component<EditorDropdownProps, EditorDropdownState>}
|
||||
*/
|
||||
@observer
|
||||
export class EditorDropdown extends React.Component<
|
||||
EditorDropdownProps,
|
||||
EditorDropdownState
|
||||
> {
|
||||
constructor(props: EditorDropdownProps) {
|
||||
super(props);
|
||||
|
||||
this.onItemClick = this.onItemClick.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Popover content={this.renderMenu()} position={Position.BOTTOM}>
|
||||
<Button icon="applications" text="Editors" />
|
||||
</Popover>
|
||||
{this.renderDocsDemos()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderDocsDemos() {
|
||||
if (!process.env.FIDDLE_DOCS_DEMOS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon="help"
|
||||
text="Docs & Demos"
|
||||
id={PanelId.docsDemo}
|
||||
onClick={this.onItemClick}
|
||||
active={!this.props.appState.closedPanels.docsDemo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMenu() {
|
||||
return <Menu>{...this.renderMenuItems()}</Menu>;
|
||||
}
|
||||
|
||||
public renderMenuItems() {
|
||||
const { appState } = this.props;
|
||||
const result: Array<JSX.Element> = [];
|
||||
const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
|
||||
|
||||
for (const id of ALL_EDITORS) {
|
||||
result.push(
|
||||
<MenuItem
|
||||
icon={visibleMosaics.includes(id) ? 'eye-open' : 'eye-off'}
|
||||
key={id}
|
||||
text={TITLE_MAP[id]}
|
||||
id={id}
|
||||
onClick={this.onItemClick}
|
||||
disabled={appState.mosaicArrangement === id} // can't hide last editor panel
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
result.push(
|
||||
<React.Fragment key={'fragment-reset-layout'}>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon="grid-view"
|
||||
key="reset-layout"
|
||||
text="Reset Layout"
|
||||
onClick={appState.resetEditorLayout}
|
||||
/>
|
||||
</React.Fragment>,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public onItemClick(event: React.MouseEvent) {
|
||||
const { id } = event.currentTarget;
|
||||
const { appState } = this.props;
|
||||
const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
|
||||
|
||||
if (visibleMosaics.includes(id as MosaicId)) {
|
||||
console.log(`EditorDropdown: Closing ${id}`);
|
||||
appState.hideAndBackupMosaic(id as MosaicId);
|
||||
} else {
|
||||
console.log(`EditorDropdown: Opening ${id}`);
|
||||
appState.showMosaic(id as MosaicId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Button, IButtonProps, Spinner } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { VersionState } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
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>}
|
||||
*/
|
||||
@observer
|
||||
export class Runner extends React.Component<RunnerProps> {
|
||||
public render() {
|
||||
const { isRunning, currentElectronVersion } = this.props.appState;
|
||||
|
||||
const state = currentElectronVersion && currentElectronVersion.state;
|
||||
const props: IButtonProps = { className: 'button-run', disabled: true };
|
||||
|
||||
if (state === VersionState.downloading) {
|
||||
props.text = 'Downloading';
|
||||
props.icon = (
|
||||
<Spinner size={16} value={currentElectronVersion?.downloadProgress} />
|
||||
);
|
||||
} else if (state === VersionState.unzipping) {
|
||||
props.text = 'Unzipping';
|
||||
props.icon = <Spinner size={16} />;
|
||||
} else if (state === VersionState.ready) {
|
||||
props.disabled = false;
|
||||
|
||||
if (isRunning) {
|
||||
props.active = true;
|
||||
props.text = 'Stop';
|
||||
props.onClick = 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.icon = <Spinner size={16} />;
|
||||
}
|
||||
|
||||
return <Button {...props} type={undefined} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ButtonGroup } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { VersionSelect } from './version-select';
|
||||
|
||||
export interface VersionChooserProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dropdown allowing the selection of Electron versions. The actual
|
||||
* download is managed in the state.
|
||||
*/
|
||||
export const VersionChooser = observer((props: VersionChooserProps) => {
|
||||
const { currentElectronVersion, Bisector, setVersion } = props.appState;
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<VersionSelect
|
||||
appState={props.appState}
|
||||
onVersionSelect={({ version }) => setVersion(version)}
|
||||
currentVersion={currentElectronVersion}
|
||||
disabled={!!Bisector}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
@@ -1,39 +1,61 @@
|
||||
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 { GistActionButton } from './commands-action-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, {}> {
|
||||
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 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>
|
||||
{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} />
|
||||
<GistActionButton appState={appState} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Button, Dialog, FileInput } from '@blueprintjs/core';
|
||||
import { shell } from 'electron';
|
||||
import * as fsType from 'fs-extra';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import { getTheme, THEMES_PATH } from '../themes';
|
||||
|
||||
import { GenericDialogType } from '../../../src/interfaces';
|
||||
import { AppState } from '../state';
|
||||
import { defaultDark, LoadedFiddleTheme } from '../themes-defaults';
|
||||
|
||||
export interface AddThemeDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface AddThemeDialogState {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "add monaco theme" dialog allows users to add custom editor themes.
|
||||
*
|
||||
* @class AddThemeDialog
|
||||
* @extends {React.Component<AddThemeDialogProps, AddThemeDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class AddThemeDialog extends React.Component<
|
||||
AddThemeDialogProps,
|
||||
AddThemeDialogState
|
||||
> {
|
||||
public resetState = { file: undefined };
|
||||
|
||||
constructor(props: AddThemeDialogProps) {
|
||||
super(props);
|
||||
this.state = this.resetState;
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onChangeFile = this.onChangeFile.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change of the file input
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
|
||||
const { files } = event.target as any;
|
||||
const file = files && files[0] ? files[0] : undefined;
|
||||
|
||||
this.setState({ file });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of the dialog
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async onSubmit(): Promise<void> {
|
||||
const { file } = this.state;
|
||||
const defaultTheme = !!this.props.appState.theme
|
||||
? await getTheme(this.props.appState.theme)
|
||||
: defaultDark;
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const editor = fsType.readJSONSync(file.path);
|
||||
if (!editor.base && !editor.rules)
|
||||
throw Error('File does not match specifications'); // has to have these attributes
|
||||
defaultTheme.editor = editor as Partial<
|
||||
MonacoType.editor.IStandaloneThemeData
|
||||
>;
|
||||
const newTheme = defaultTheme;
|
||||
const name = editor.name ? editor.name : file.name;
|
||||
await this.createNewThemeFromMonaco(name, newTheme);
|
||||
} catch (error) {
|
||||
this.props.appState.setGenericDialogOptions({
|
||||
type: GenericDialogType.warning,
|
||||
label: `Error: ${error}, please pick a different file.`,
|
||||
});
|
||||
this.props.appState.isGenericDialogShowing = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
public async createNewThemeFromMonaco(
|
||||
name: string,
|
||||
newTheme: LoadedFiddleTheme,
|
||||
): Promise<boolean> {
|
||||
if (!name) return false;
|
||||
const themePath = path.join(THEMES_PATH, `${name}`);
|
||||
|
||||
await fsType.outputJSON(
|
||||
themePath,
|
||||
{
|
||||
...newTheme,
|
||||
name,
|
||||
},
|
||||
{ spaces: 2 },
|
||||
);
|
||||
|
||||
this.props.appState.setTheme(themePath);
|
||||
shell.showItemInFolder(themePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
const canSubmit = !!this.state.file;
|
||||
|
||||
return [
|
||||
<Button
|
||||
icon="add"
|
||||
key="submit"
|
||||
disabled={!canSubmit}
|
||||
onClick={this.onSubmit}
|
||||
text="Add"
|
||||
/>,
|
||||
<Button icon="cross" key="cancel" onClick={this.onClose} text="Cancel" />,
|
||||
];
|
||||
}
|
||||
|
||||
public onClose() {
|
||||
this.props.appState.isThemeDialogShowing = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isThemeDialogShowing } = this.props.appState;
|
||||
const inputProps = { accept: '.json' };
|
||||
const { file } = this.state;
|
||||
|
||||
const text = file && file.path ? file.path : `Select the Monaco file...`;
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isThemeDialogShowing}
|
||||
onClose={this.onClose}
|
||||
title="Add theme"
|
||||
className="dialog-add-version"
|
||||
>
|
||||
<div className="bp3-dialog-body">
|
||||
<FileInput
|
||||
onInputChange={this.onChangeFile}
|
||||
inputProps={inputProps as any}
|
||||
text={text}
|
||||
/>
|
||||
<br />
|
||||
</div>
|
||||
<div className="bp3-dialog-footer">
|
||||
<div className="bp3-dialog-footer-actions">{this.buttons}</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this component's state
|
||||
*/
|
||||
private reset(): void {
|
||||
this.setState(this.resetState);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
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 { Version } from '../../interfaces';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { getElectronNameForPlatform } from '../../utils/electron-name';
|
||||
import { getIsDownloaded } from '../binary';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface AddVersionDialogProps {
|
||||
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 isValidElectron = !!(await 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: Version = {
|
||||
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,234 @@
|
||||
import { Button, ButtonGroup, Callout, Dialog, Label } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { RunnableVersion } from '../../interfaces';
|
||||
import { Bisector } from '../bisect';
|
||||
import { AppState } from '../state';
|
||||
import { VersionSelect } from './version-select';
|
||||
|
||||
export interface BisectDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface BisectDialogState {
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
allVersions: Array<RunnableVersion>;
|
||||
showHelp?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "add version" dialog allows users to add custom builds of Electron.
|
||||
*
|
||||
* @class BisectDialog
|
||||
* @extends {React.Component<BisectDialogProps, BisectDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class BisectDialog extends React.Component<
|
||||
BisectDialogProps,
|
||||
BisectDialogState
|
||||
> {
|
||||
constructor(props: BisectDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onBeginSelect = this.onBeginSelect.bind(this);
|
||||
this.onEndSelect = this.onEndSelect.bind(this);
|
||||
this.showHelp = this.showHelp.bind(this);
|
||||
this.isEarliestItemDisabled = this.isEarliestItemDisabled.bind(this);
|
||||
this.isLatestItemDisabled = this.isLatestItemDisabled.bind(this);
|
||||
|
||||
this.state = {
|
||||
allVersions: this.props.appState.versionsToShow,
|
||||
startIndex: 10,
|
||||
endIndex: 0,
|
||||
};
|
||||
}
|
||||
|
||||
public onBeginSelect(version: RunnableVersion) {
|
||||
this.setState({ startIndex: this.state.allVersions.indexOf(version) });
|
||||
}
|
||||
|
||||
public onEndSelect(version: RunnableVersion) {
|
||||
this.setState({ endIndex: this.state.allVersions.indexOf(version) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of the dialog
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async onSubmit(): Promise<void> {
|
||||
const { endIndex, startIndex, allVersions } = this.state;
|
||||
const { appState } = this.props;
|
||||
|
||||
if (endIndex === undefined || startIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bisectRange = allVersions.slice(endIndex, startIndex + 1).reverse();
|
||||
|
||||
appState.Bisector = new Bisector(bisectRange);
|
||||
const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
|
||||
appState.setVersion(initialBisectPivot);
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog
|
||||
*/
|
||||
public onClose() {
|
||||
this.props.appState.isBisectDialogShowing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the additional help
|
||||
*/
|
||||
public showHelp() {
|
||||
this.setState({ showHelp: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Can we get this show on the road?
|
||||
*/
|
||||
get canSubmit(): boolean {
|
||||
return this.state.startIndex > this.state.endIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the buttons
|
||||
*/
|
||||
get buttons() {
|
||||
return [
|
||||
<Button
|
||||
icon="play"
|
||||
key="submit"
|
||||
disabled={!this.canSubmit}
|
||||
onClick={this.onSubmit}
|
||||
text="Begin"
|
||||
/>,
|
||||
<Button icon="cross" key="cancel" onClick={this.onClose} text="Cancel" />,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the help
|
||||
*/
|
||||
get help() {
|
||||
let moreHelp = (
|
||||
<Button icon="help" text="Show help" onClick={this.showHelp} />
|
||||
);
|
||||
|
||||
if (this.state.showHelp) {
|
||||
moreHelp = (
|
||||
<>
|
||||
<p>
|
||||
First, write a fiddle that reproduces a bug or an issue. Then,
|
||||
select the earliest version to start your search with. Typically,
|
||||
that's the "last known good" version that did not
|
||||
have the bug. Then, select that latest version to end the search
|
||||
with, usually the "first known bad" version.
|
||||
</p>
|
||||
<p>
|
||||
Once you begin your bisect, Fiddle will run your fiddle with a
|
||||
number of Electron versions, closing in on the version that
|
||||
introduced the bug. Once completed, you will know which Electron
|
||||
version introduced your issue.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Callout style={{ marginTop: 0, marginBottom: '1rem' }}>
|
||||
<p>
|
||||
A "bisect" is a popular method{' '}
|
||||
<a
|
||||
href="https://git-scm.com/docs/git-bisect"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
borrowed from <code>git</code>
|
||||
</a>{' '}
|
||||
for learning which version of Electron introduced a bug. This tool
|
||||
helps you perform a bisect.
|
||||
</p>
|
||||
{moreHelp}
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isBisectDialogShowing } = this.props.appState;
|
||||
const { startIndex, endIndex, allVersions } = this.state;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isBisectDialogShowing}
|
||||
onClose={this.onClose}
|
||||
title="Start a bisect session"
|
||||
className="dialog-add-version"
|
||||
>
|
||||
<div className="bp3-dialog-body">
|
||||
{this.help}
|
||||
<Label>
|
||||
Earliest Version (Last "known good" version)
|
||||
<ButtonGroup fill={true}>
|
||||
<VersionSelect
|
||||
currentVersion={allVersions[startIndex]}
|
||||
appState={this.props.appState}
|
||||
onVersionSelect={this.onBeginSelect}
|
||||
itemDisabled={this.isEarliestItemDisabled}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Label>
|
||||
<Label>
|
||||
Latest Version (First "known bad" version)
|
||||
<ButtonGroup fill={true}>
|
||||
<VersionSelect
|
||||
currentVersion={allVersions[endIndex]}
|
||||
appState={this.props.appState}
|
||||
onVersionSelect={this.onEndSelect}
|
||||
itemDisabled={this.isLatestItemDisabled}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="bp3-dialog-footer">
|
||||
<div className="bp3-dialog-footer-actions">{this.buttons}</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should an item in the "earliest version" dropdown be disabled?
|
||||
*
|
||||
* @param {RunnableVersion} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isEarliestItemDisabled(version: RunnableVersion): boolean {
|
||||
const { allVersions, endIndex } = this.state;
|
||||
|
||||
// In the array, "newer" versions will have a lower index.
|
||||
// 0: 5.0.0
|
||||
// 1: 4.0.0
|
||||
// 2: 3.0.0
|
||||
// ...
|
||||
return allVersions.indexOf(version) < endIndex + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should an item in the "latest version" dropdown be disabled?
|
||||
*
|
||||
* @param {RunnableVersion} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isLatestItemDisabled(version: RunnableVersion): boolean {
|
||||
const { allVersions, startIndex } = this.state;
|
||||
|
||||
return allVersions.indexOf(version) > startIndex - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Alert, IconName, InputGroup, Intent } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GenericDialogType } from '../../../src/interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface GenericDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The token dialog prompts the user to either continue or cancel the operation.
|
||||
*
|
||||
* @export
|
||||
* @class GenericDialog
|
||||
* @extends {React.Component<GenericDialogProps, GenericDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class GenericDialog extends React.Component<GenericDialogProps> {
|
||||
constructor(props: GenericDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
public onClose(result: boolean) {
|
||||
const input = document.getElementById('input') as HTMLInputElement;
|
||||
|
||||
this.props.appState.genericDialogLastInput =
|
||||
input && input.value !== '' ? input.value : null;
|
||||
this.props.appState.genericDialogLastResult = result;
|
||||
this.props.appState.toggleGenericDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
isGenericDialogShowing,
|
||||
genericDialogOptions,
|
||||
} = this.props.appState;
|
||||
const {
|
||||
type,
|
||||
ok,
|
||||
cancel,
|
||||
label,
|
||||
wantsInput,
|
||||
placeholder,
|
||||
} = genericDialogOptions;
|
||||
|
||||
let intent: Intent;
|
||||
let icon: IconName;
|
||||
switch (type) {
|
||||
case GenericDialogType.warning:
|
||||
intent = Intent.DANGER;
|
||||
icon = 'warning-sign';
|
||||
break;
|
||||
case GenericDialogType.confirm:
|
||||
intent = Intent.PRIMARY;
|
||||
icon = 'help';
|
||||
break;
|
||||
case GenericDialogType.success:
|
||||
intent = Intent.SUCCESS;
|
||||
icon = 'info-sign';
|
||||
break;
|
||||
default:
|
||||
intent = Intent.NONE;
|
||||
icon = 'help';
|
||||
break;
|
||||
}
|
||||
|
||||
let dialogInput;
|
||||
if (wantsInput) {
|
||||
dialogInput = placeholder ? (
|
||||
<InputGroup id="input" placeholder={placeholder} />
|
||||
) : (
|
||||
<InputGroup id="input" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
isOpen={isGenericDialogShowing}
|
||||
onClose={this.onClose}
|
||||
icon={icon}
|
||||
confirmButtonText={ok}
|
||||
cancelButtonText={cancel}
|
||||
intent={intent}
|
||||
>
|
||||
<p>{label}</p>
|
||||
{wantsInput && dialogInput}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
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 });
|
||||
this.props.appState.gitHubToken = null;
|
||||
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,64 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { AddThemeDialog } from './dialog-add-theme';
|
||||
import { AddVersionDialog } from './dialog-add-version';
|
||||
import { BisectDialog } from './dialog-bisect';
|
||||
import { GenericDialog } from './dialog-generic';
|
||||
import { TokenDialog } from './dialog-token';
|
||||
import { 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,
|
||||
isGenericDialogShowing,
|
||||
} = 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 genericDialog = isGenericDialogShowing ? (
|
||||
<GenericDialog appState={appState} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div key="dialogs" className="dialogs">
|
||||
{maybeToken}
|
||||
{maybeSettings}
|
||||
{maybeAddLocalVersion}
|
||||
{maybeMonaco}
|
||||
{maybeBisect}
|
||||
{genericDialog}
|
||||
</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,167 @@
|
||||
// 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;
|
||||
setFocused: (id: EditorId) => void;
|
||||
}
|
||||
|
||||
export class Editor extends React.Component<EditorProps> {
|
||||
public editor: MonacoType.editor.IStandaloneCodeEditor;
|
||||
public language = 'javascript';
|
||||
public value = '';
|
||||
|
||||
private containerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: EditorProps) {
|
||||
super(props);
|
||||
|
||||
switch (props.id) {
|
||||
case 'html':
|
||||
this.language = 'html';
|
||||
break;
|
||||
case 'css':
|
||||
this.language = 'css';
|
||||
break;
|
||||
default:
|
||||
this.language = 'javascript';
|
||||
}
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
await 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,
|
||||
});
|
||||
|
||||
// mark this editor as focused whenever it is
|
||||
this.editor.onDidFocusEditorText(() => {
|
||||
const { id, setFocused } = this.props;
|
||||
setFocused(id);
|
||||
});
|
||||
|
||||
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} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model and attach it to the editor
|
||||
*
|
||||
* @private
|
||||
* @param {string} value
|
||||
*/
|
||||
private createModel(value: string) {
|
||||
const { monaco } = this.props;
|
||||
|
||||
const model = monaco.editor.createModel(value, this.language);
|
||||
model.updateOptions({
|
||||
tabSize: 2,
|
||||
});
|
||||
|
||||
this.editor.setModel(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content on the editor, including the model and the view state.
|
||||
*
|
||||
* @private
|
||||
* @memberof Editor
|
||||
*/
|
||||
private async setContent() {
|
||||
const { appState, id } = this.props;
|
||||
const { version } = appState;
|
||||
|
||||
const backup = appState.getAndRemoveEditorValueBackup(id);
|
||||
|
||||
if (backup) {
|
||||
console.log(`Editor: Backup found, restoring state`);
|
||||
|
||||
if (backup.viewState) {
|
||||
this.editor.restoreViewState(backup.viewState);
|
||||
}
|
||||
|
||||
// If there's a model, use the model. No model? Use the value
|
||||
if (backup.model) {
|
||||
this.editor.setModel(backup.model);
|
||||
} else {
|
||||
this.createModel(backup.value ?? '');
|
||||
}
|
||||
} else {
|
||||
const value = await getContent(id, version);
|
||||
this.createModel(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, NonIdealState } from '@blueprintjs/core';
|
||||
|
||||
import { EditorId } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export function renderNonIdealState(appState: AppState) {
|
||||
const allEditors = [EditorId.html, EditorId.main, EditorId.renderer];
|
||||
const resolveButton = (
|
||||
<Button
|
||||
text="Open all editors"
|
||||
onClick={() => appState.setVisibleMosaics(allEditors)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NonIdealState
|
||||
action={resolveButton}
|
||||
icon="applications"
|
||||
description='You have closed all editors. You can open them again with the button below or the "Editors" button above!'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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,356 @@
|
||||
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)',
|
||||
css: 'Stylesheet (styles.css)',
|
||||
docsDemo: 'Docs & Demos',
|
||||
};
|
||||
|
||||
export interface EditorsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface EditorsState {
|
||||
monaco?: typeof MonacoType;
|
||||
isMounted?: boolean;
|
||||
monacoOptions: MonacoType.editor.IEditorOptions;
|
||||
focused?: EditorId;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
// A reaction: Each time mosaicArrangement is changed, we'll update
|
||||
// the editor layout. That method is itself debounced.
|
||||
public disposeLayoutAutorun = reaction(
|
||||
() => this.props.appState.mosaicArrangement,
|
||||
() => updateEditorLayout(),
|
||||
);
|
||||
|
||||
constructor(props: EditorsProps) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.renderEditor = this.renderEditor.bind(this);
|
||||
this.renderTile = this.renderTile.bind(this);
|
||||
this.renderGenericPanel = this.renderGenericPanel.bind(this);
|
||||
this.setFocused = this.setFocused.bind(this);
|
||||
|
||||
this.state = { monacoOptions: defaultMonacoOptions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed right after the component mounts. We'll setup the IPC listeners here.
|
||||
*
|
||||
* @memberof Editors
|
||||
*/
|
||||
public async componentDidMount() {
|
||||
ipcRendererManager.on(
|
||||
IpcEvents.MONACO_EXECUTE_COMMAND,
|
||||
(_event, cmd: string) => {
|
||||
this.executeCommand(cmd);
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
||||
ipcRendererManager.on(IpcEvents.SELECT_ALL_IN_EDITOR, (_event) => {
|
||||
// programmatically fetch all editor contents and set as selection
|
||||
const editor = getFocusedEditor();
|
||||
const range = editor?.getModel()?.getFullModelRange();
|
||||
|
||||
if (!!range) {
|
||||
editor?.setSelection(range);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ isMounted: true });
|
||||
await this.loadMonaco();
|
||||
this.props.appState.isUnsaved = false;
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.disposeLayoutAutorun();
|
||||
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_EXECUTE_COMMAND);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.FS_NEW_FIDDLE);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_TOGGLE_OPTION);
|
||||
ipcRendererManager.removeAllListeners(IpcEvents.SELECT_ALL_IN_EDITOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { appState } = this.props;
|
||||
const docsDemoGoHomeMaybe =
|
||||
id === PanelId.docsDemo ? (
|
||||
<DocsDemoGoHomeButton id={id} appState={appState} />
|
||||
) : null;
|
||||
|
||||
// only show toolbar controls if we have more than 1 visible editor
|
||||
// Mosaic arrangement is type string if 1 editor, object otherwise
|
||||
const toolbarControlsMaybe = typeof appState.mosaicArrangement !==
|
||||
'string' && (
|
||||
<>
|
||||
<MaximizeButton id={id} appState={appState} />
|
||||
<RemoveButton id={id} appState={appState} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Left */}
|
||||
<div>
|
||||
<h5>{title}</h5>
|
||||
</div>
|
||||
{/* Middle */}
|
||||
<div />
|
||||
{/* Right */}
|
||||
<div className="mosaic-controls">
|
||||
{docsDemoGoHomeMaybe}
|
||||
{toolbarControlsMaybe}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Mosaic tile
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {string} path
|
||||
* @returns {JSX.Element | null}
|
||||
*/
|
||||
public renderTile(id: MosaicId, path: Array<MosaicBranch>): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const content = isEditorId(id)
|
||||
? this.renderEditor(id)
|
||||
: this.renderGenericPanel(id, appState);
|
||||
|
||||
return (
|
||||
<MosaicWindow<EditorId>
|
||||
className={id}
|
||||
path={path}
|
||||
title={TITLE_MAP[id]}
|
||||
renderToolbar={(props: MosaicWindowProps<MosaicId>) =>
|
||||
this.renderToolbar(props, id)
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</MosaicWindow>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a generic panel – so not an editor, but something else
|
||||
*
|
||||
* @param {PanelId} _id
|
||||
* @param {AppState} _appState
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
public renderGenericPanel(_id: PanelId, appState: AppState): JSX.Element {
|
||||
return <ShowMe appState={appState} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an editor
|
||||
*
|
||||
* @param {EditorId} id
|
||||
* @returns {(JSX.Element | null)}
|
||||
* @memberof Editors
|
||||
*/
|
||||
public renderEditor(id: EditorId): JSX.Element | null {
|
||||
const { appState } = this.props;
|
||||
const { monaco } = this.state;
|
||||
|
||||
return (
|
||||
<Editor
|
||||
id={id}
|
||||
monaco={monaco!}
|
||||
appState={appState}
|
||||
monacoOptions={defaultMonacoOptions}
|
||||
setFocused={this.setFocused}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { monaco } = this.state;
|
||||
|
||||
if (!monaco) return null;
|
||||
|
||||
return (
|
||||
<Mosaic<EditorId | PanelId>
|
||||
className={`focused__${this.state.focused}`}
|
||||
onChange={this.onChange}
|
||||
value={appState.mosaicArrangement}
|
||||
zeroStateView={renderNonIdealState(appState)}
|
||||
renderTile={this.renderTile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in the visible nodes
|
||||
*
|
||||
* @param {(MosaicNode<EditorId> | null)} currentNode
|
||||
*/
|
||||
public onChange(currentNode: MosaicNode<EditorId> | null) {
|
||||
this.props.appState.mosaicArrangement = currentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads monaco. If it's already loaded, it'll just set it on the current state.
|
||||
* We're doing things a bit roundabout to ensure that we're not overloading the
|
||||
* mobx state with a gigantic Monaco tree.
|
||||
*/
|
||||
public async loadMonaco() {
|
||||
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.setState({
|
||||
monaco,
|
||||
monacoOptions: defaultMonacoOptions,
|
||||
});
|
||||
} else {
|
||||
this.setState({ monaco });
|
||||
}
|
||||
|
||||
await activateTheme(monaco, undefined, this.props.appState.theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currently-focused editor. This will impact the editor's
|
||||
* z-index, ensuring that its intellisense menus don't get clipped
|
||||
* by the other editor windows.
|
||||
* @param id Editor ID
|
||||
*/
|
||||
public setFocused(id: EditorId): void {
|
||||
this.setState({ focused: id });
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
export class Header extends React.Component<HeaderProps, {}> {
|
||||
|
||||
/**
|
||||
* Everything above the editors, so buttons and the address bar.
|
||||
*
|
||||
* @class Header
|
||||
* @extends {React.Component<HeaderProps>}
|
||||
*/
|
||||
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,61 @@
|
||||
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,114 @@
|
||||
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, {}> {
|
||||
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();
|
||||
const timestamp = <span className='timestamp'>{ts}</span>;
|
||||
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,110 @@
|
||||
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,402 @@
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Callout,
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
HTMLTable,
|
||||
IButtonProps,
|
||||
Icon,
|
||||
IconName,
|
||||
Tooltip,
|
||||
} from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { RunnableVersion, VersionSource, VersionState } 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 VersionState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.channelsToShow = appState.channelsToShow.filter((c) => c !== id);
|
||||
} else {
|
||||
appState.channelsToShow.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: VersionState) => {
|
||||
return appState.statesToShow.includes(state);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup label="Include Electron versions that are:">
|
||||
<Tooltip content="Always enabled" position="bottom" intent="primary">
|
||||
<Checkbox
|
||||
checked={getIsChecked(VersionState.ready)}
|
||||
label="Ready"
|
||||
id="ready"
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
disabled={true}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Checkbox
|
||||
checked={getIsChecked(VersionState.downloading)}
|
||||
label="Downloading"
|
||||
id="downloading"
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(VersionState.unknown)}
|
||||
label="Not Downloaded"
|
||||
id="unknown"
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the various options for which versions should be displayed
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderVersionChannelOptions(): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
|
||||
const getIsChecked = (channel: ElectronReleaseChannel) => {
|
||||
return appState.channelsToShow.includes(channel);
|
||||
};
|
||||
|
||||
const getIsCurrentVersionReleaseChannel = (
|
||||
channel: ElectronReleaseChannel,
|
||||
) => {
|
||||
return getReleaseChannel(appState.version) === channel;
|
||||
};
|
||||
|
||||
const channels = {
|
||||
stable: ElectronReleaseChannel.stable,
|
||||
beta: ElectronReleaseChannel.beta,
|
||||
nightly: ElectronReleaseChannel.nightly,
|
||||
unsupported: ElectronReleaseChannel.unsupported,
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup label="Include Electron versions from these release channels:">
|
||||
{Object.entries(channels).map(([_, channel]) => (
|
||||
<Tooltip
|
||||
content={`Can't disable channel of selected version (${appState.version})`}
|
||||
disabled={!getIsCurrentVersionReleaseChannel(channel)}
|
||||
position="bottom"
|
||||
intent="primary"
|
||||
key={channel}
|
||||
>
|
||||
<Checkbox
|
||||
checked={getIsChecked(channel)}
|
||||
label={channel}
|
||||
id={channel}
|
||||
onChange={this.handleChannelChange}
|
||||
disabled={getIsCurrentVersionReleaseChannel(channel)}
|
||||
inline={true}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, channelsToShow, statesToShow } = this.props.appState;
|
||||
|
||||
return sortedElectronMap<JSX.Element | null>(versions, (key, item) => {
|
||||
// Check if we want to show the version
|
||||
if (!channelsToShow.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 {RunnableVersion} item
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderHumanState(item: RunnableVersion): JSX.Element {
|
||||
const { state } = item;
|
||||
let icon: IconName = 'box';
|
||||
let humanState = 'Downloaded';
|
||||
|
||||
if (state === VersionState.downloading) {
|
||||
icon = 'cloud-download';
|
||||
humanState = 'Downloading';
|
||||
} else if (state === VersionState.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 {RunnableVersion} item
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderAction(key: string, item: RunnableVersion): 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 === VersionSource.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,170 @@
|
||||
import {
|
||||
Callout,
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
} from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { IPackageManager } from '../npm';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ExecutionSettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage execution-related preferences.
|
||||
*
|
||||
* @class ExecutionSettings
|
||||
* @extends {React.Component<ExecutionSettingsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class ExecutionSettings extends React.Component<ExecutionSettingsProps> {
|
||||
constructor(props: ExecutionSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.handleDeleteDataChange = this.handleDeleteDataChange.bind(this);
|
||||
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(
|
||||
this,
|
||||
);
|
||||
this.handleExecutionFlagChange = this.handleExecutionFlagChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change on whether or not the user data dir should be deleted
|
||||
* after a run.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleDeleteDataChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
const { checked } = event.currentTarget;
|
||||
this.props.appState.isKeepingUserDataDirs = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change on whether or not electron should log more things
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleElectronLoggingChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
const { checked } = event.currentTarget;
|
||||
this.props.appState.isEnablingElectronLogging = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in the execution flags run with the Electron executable
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleExecutionFlagChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
const { value } = event.currentTarget;
|
||||
const flags = value.split('|');
|
||||
this.props.appState.executionFlags = flags;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
isKeepingUserDataDirs,
|
||||
isEnablingElectronLogging,
|
||||
executionFlags = [],
|
||||
} = this.props.appState;
|
||||
|
||||
const deleteUserDirLabel = `
|
||||
Whenever Electron runs, it creates a user data directory for cookies, the cache,
|
||||
and various other things that it needs to keep around. Since fiddles are usually
|
||||
just run once, we delete this directory after your fiddle exits. Enable this
|
||||
setting to keep the user data directories around.
|
||||
`.trim();
|
||||
const electronLoggingLabel = `
|
||||
There are some flags that Electron uses to log extra information both internally
|
||||
and through Chromium. Enable this option to make Fiddle produce those logs.`.trim();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Execution</h2>
|
||||
<Callout>
|
||||
These advanced settings control how Electron Fiddle executes your
|
||||
fiddles.
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
<FormGroup label={deleteUserDirLabel}>
|
||||
<Checkbox
|
||||
checked={isKeepingUserDataDirs}
|
||||
label="Do not delete user data directories."
|
||||
onChange={this.handleDeleteDataChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
<FormGroup label={electronLoggingLabel}>
|
||||
<Checkbox
|
||||
checked={isEnablingElectronLogging}
|
||||
label="Enable advanced Electron logging."
|
||||
onChange={this.handleElectronLoggingChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
<FormGroup>
|
||||
<p>
|
||||
Electron allows starting the executable with{' '}
|
||||
<a href="https://www.electronjs.org/docs/api/command-line-switches">
|
||||
user-provided flags
|
||||
</a>
|
||||
, such as '--js-flags=--expose-gc'. Those can be added
|
||||
here as bar-separated (|) flags to run when you start your
|
||||
Fiddles.
|
||||
</p>
|
||||
<br />
|
||||
<InputGroup
|
||||
placeholder="--js-flags=--expose-gc|--lang=es"
|
||||
value={executionFlags.join('|')}
|
||||
onChange={this.handleExecutionFlagChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
<FormGroup>
|
||||
<span style={{ marginRight: 4 }}>
|
||||
Electron Fiddle will install packages on runtime if they are
|
||||
imported within your fiddle with <code>require</code>. It uses{' '}
|
||||
<a href="https://www.npmjs.com/" target="_blank" rel="noreferrer">
|
||||
npm
|
||||
</a>{' '}
|
||||
as its package manager by default, but{' '}
|
||||
<a
|
||||
href="https://classic.yarnpkg.com/lang/en/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Yarn
|
||||
</a>{' '}
|
||||
is also available.
|
||||
</span>
|
||||
<RadioGroup
|
||||
onChange={this.handlePMChange}
|
||||
selectedValue={this.props.appState.packageManager}
|
||||
inline={true}
|
||||
>
|
||||
<Radio label="npm" value="npm" />
|
||||
<Radio label="yarn" value="yarn" />
|
||||
</RadioGroup>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handlePMChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
this.props.appState.packageManager = event.currentTarget
|
||||
.value as IPackageManager;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
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 {RunnableVersion} { 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 {RunnableVersion} 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;
|
||||
toggleHasPopoverOpen: () => void;
|
||||
}
|
||||
|
||||
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}
|
||||
popoverProps={{
|
||||
onClosed: () => this.props.toggleHasPopoverOpen(),
|
||||
}}
|
||||
noResults={<MenuItem disabled={true} text="No results." />}
|
||||
>
|
||||
<Button
|
||||
id="open-theme-selector"
|
||||
text={selectedName}
|
||||
icon="tint"
|
||||
onClick={() => this.props.toggleHasPopoverOpen()}
|
||||
/>
|
||||
</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,58 @@
|
||||
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,38 @@
|
||||
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;
|
||||
toggleHasPopoverOpen: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
toggleHasPopoverOpen={() => this.props.toggleHasPopoverOpen()}
|
||||
/>
|
||||
<Divider />
|
||||
<ConsoleSettings appState={this.props.appState} />
|
||||
<Divider />
|
||||
<GitHubSettings appState={this.props.appState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
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;
|
||||
hasPopoverOpen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
hasPopoverOpen: false,
|
||||
};
|
||||
|
||||
this.closeSettingsPanel = this.closeSettingsPanel.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener('keyup', this.closeSettingsPanel, true);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('keyup', this.closeSettingsPanel, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
toggleHasPopoverOpen={() => this.toggleHasPopoverOpen()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
* If hasPopoverOpen is set to true, settings will not close as only the popover should close
|
||||
*/
|
||||
private closeSettingsPanel(event: KeyboardEvent) {
|
||||
const { appState } = this.props;
|
||||
if (event.code === 'Escape' && !this.state.hasPopoverOpen) {
|
||||
appState.isSettingsShowing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles whether there is a popover open
|
||||
*/
|
||||
public toggleHasPopoverOpen(): void {
|
||||
this.setState({
|
||||
...this.state,
|
||||
hasPopoverOpen: !this.state.hasPopoverOpen,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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>}
|
||||
*/
|
||||
@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,141 @@
|
||||
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(): 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,47 @@
|
||||
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',
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user