Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,5 +1,5 @@
|
||||
language: node_js
|
||||
node_js: "8"
|
||||
node_js: "12"
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
@@ -25,9 +25,40 @@ branches:
|
||||
|
||||
install:
|
||||
- npm install
|
||||
- |
|
||||
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 -- --runInBand
|
||||
- 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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 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
|
||||
|
||||
[](https://travis-ci.org/electron/fiddle)
|
||||
[](https://travis-ci.org/electron/fiddle) [](https://coveralls.io/github/electron/fiddle?branch=master)
|
||||
|
||||
Electron Fiddle lets you create and play with small Electron experiments. It
|
||||
greets you with a quick-start template after opening – change a few things,
|
||||
choose the version of Electron you want to run it with, and play around. Then,
|
||||
save your Fiddle either as a GitHub Gist or to a local folder. Once pushed to
|
||||
GitHub, anyone can quickly try your Fiddle out by just entering it in the
|
||||
save your Fiddle either as a GitHub Gist or to a local folder. Once published
|
||||
on GitHub, anyone can quickly try your Fiddle out by just entering it in the
|
||||
address bar.
|
||||
|
||||
<h3 align="center">
|
||||
@@ -15,13 +15,13 @@ address bar.
|
||||
</a>
|
||||
</h3>
|
||||
|
||||

|
||||
<img src="https://user-images.githubusercontent.com/1426799/52155868-d3357c80-2639-11e9-9496-fa97b1dc7897.jpg" width="880px" alt="Electron Fiddle screenshot">
|
||||
|
||||
# Features
|
||||
|
||||
### Explore Electron
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
Fiddle can automatically turn your experiment into binaries you can share with
|
||||
your friends, coworkers, or grandparents. It does so thanks to
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "10"
|
||||
|
||||
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
|
||||
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 968 KiB 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 |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 23 KiB |
@@ -1,7 +1,11 @@
|
||||
/* tslint:disable */
|
||||
|
||||
const path = require('path');
|
||||
const package = require('./package.json');
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const { version } = packageJson
|
||||
const iconDir = path.resolve(__dirname, 'assets', 'icons')
|
||||
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -29,24 +33,34 @@ module.exports = {
|
||||
},
|
||||
osxSign: {
|
||||
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)'
|
||||
},
|
||||
}
|
||||
},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
platforms: ['win32'],
|
||||
config: {
|
||||
name: 'electron-fiddle',
|
||||
authors: 'Electron Community',
|
||||
exe: 'electron-fiddle.exe',
|
||||
iconUrl: 'https://raw.githubusercontent.com/electron/fiddle/b5bf652df0ba159ec40a62001dbb298649c3b985/assets/icons/fiddle.ico',
|
||||
loadingGif: './assets/loading.gif',
|
||||
noMsi: true,
|
||||
remoteReleases: '',
|
||||
setupExe: `electron-fiddle-${package.version}-setup-${process.arch}.exe`,
|
||||
setupIcon: path.resolve(__dirname, 'assets', 'icons', 'fiddle.ico'),
|
||||
certificateFile: process.env.WINDOWS_CERTIFICATE_FILE,
|
||||
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -55,7 +69,12 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-deb',
|
||||
platforms: ['linux']
|
||||
platforms: ['linux'],
|
||||
config: {
|
||||
icon: {
|
||||
scalable: path.resolve(iconDir, 'fiddle.svg')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-rpm',
|
||||
@@ -70,8 +89,9 @@ module.exports = {
|
||||
owner: 'electron',
|
||||
name: 'fiddle'
|
||||
},
|
||||
prerelease: true
|
||||
draft: true,
|
||||
prerelease: false
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
"bail": true,
|
||||
"resetMocks": true,
|
||||
"resetModules": true,
|
||||
"setupTestFrameworkScriptFile": "<rootDir>/tests/setup.js",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/tests/setup.js"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"jsx",
|
||||
@@ -20,11 +22,13 @@
|
||||
],
|
||||
"coverageReporters": [
|
||||
"json",
|
||||
"html"
|
||||
"html",
|
||||
"lcov"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"!**/*.d.ts"
|
||||
"!**/*.d.ts",
|
||||
"!**/*constants.ts"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "electron-fiddle",
|
||||
"productName": "Electron Fiddle",
|
||||
"version": "0.3.0",
|
||||
"version": "0.9.0",
|
||||
"description": "The easiest way to get started with Electron",
|
||||
"repository": "https://github.com/electron/fiddle",
|
||||
"main": "./dist/main/main",
|
||||
"main": "./dist/src/main/main",
|
||||
"scripts": {
|
||||
"contributors": "node ./tools/contributors.js",
|
||||
"less": "node ./tools/lessc.js",
|
||||
"lint:style": "stylelint ./src/less/*.less --fix",
|
||||
"lint:ts": "tslint -c tslint.json -p tsconfig.json -e \"node_modules/**/*.ts\" --fix",
|
||||
"lint:tests": "tslint ./tests/**/*.ts{,x} -c tslint.json --fix",
|
||||
"lint:templates": "standard ./static/templates/**/*.js",
|
||||
"lint:templates": "standard ./static/show-me/**/*.js",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"make": "electron-forge make",
|
||||
"package": "electron-forge package",
|
||||
@@ -20,6 +20,8 @@
|
||||
"publish": "electron-forge publish",
|
||||
"start": "rimraf ./dist && electron-forge start",
|
||||
"test": "jest --config=jest.json --coverage",
|
||||
"test:ci": "jest --config=jest.json --coverage --runInBand",
|
||||
"test:coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
|
||||
"tsc": "node ./tools/tsc.js",
|
||||
"electron-releases": "node ./tools/fetch-releases.js"
|
||||
},
|
||||
@@ -34,71 +36,73 @@
|
||||
"forge": "./forge.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.13",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.3.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.3",
|
||||
"@octokit/rest": "^15.12.1",
|
||||
"builtin-modules": "^3.0.0",
|
||||
"@blueprintjs/core": "^3.17.1",
|
||||
"@blueprintjs/select": "^3.9.0",
|
||||
"@octokit/rest": "^16.28.2",
|
||||
"builtin-modules": "^3.1.0",
|
||||
"classnames": "^2.2.6",
|
||||
"electron-default-menu": "^1.0.1",
|
||||
"electron-devtools-installer": "^2.2.4",
|
||||
"electron-download": "^4.1.1",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"extract-zip": "^1.6.7",
|
||||
"fix-path": "^2.1.0",
|
||||
"fs-extra": "^7.0.0",
|
||||
"loader-utils": "^1.1.0",
|
||||
"mobx": "^5.5.0",
|
||||
"mobx-react": "^5.2.8",
|
||||
"monaco-loader": "^0.14.0",
|
||||
"namor": "^1.1.1",
|
||||
"react": "^16.5.2",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-mosaic-component": "^1.1.1",
|
||||
"semver": "^5.5.1",
|
||||
"tmp": "0.0.33",
|
||||
"tslib": "^1.9.3",
|
||||
"update-electron-app": "^1.3.0"
|
||||
"fs-extra": "^8.1.0",
|
||||
"mobx": "^5.11.0",
|
||||
"mobx-react": "^6.1.1",
|
||||
"monaco-editor": "^0.17.1",
|
||||
"monaco-loader": "1.0.0",
|
||||
"namor": "^1.1.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-mosaic-component": "^3.2.0",
|
||||
"semver": "^6.2.0",
|
||||
"tmp": "0.1.0",
|
||||
"tslib": "^1.10.0",
|
||||
"update-electron-app": "^1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.0.0-beta.28",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.28",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.28",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.28",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.28",
|
||||
"@types/builtin-modules": "^2.0.0",
|
||||
"@types/enzyme": "^3.1.13",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/jest": "^23.3.2",
|
||||
"@types/log-symbols": "^2.0.0",
|
||||
"@babel/core": "^7.4.5",
|
||||
"@electron-forge/cli": "^6.0.0-beta.39",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.39",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.39",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.39",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.39",
|
||||
"@electron-forge/publisher-github": "^6.0.0-beta.39",
|
||||
"@types/builtin-modules": "^3.1.1",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/enzyme": "^3.10.0",
|
||||
"@types/fs-extra": "^8.0.0",
|
||||
"@types/jest": "^24.0.15",
|
||||
"@types/log-symbols": "^3.0.0",
|
||||
"@types/node": "^10.11.1",
|
||||
"@types/react": "^16.4.14",
|
||||
"@types/react-dom": "^16.0.8",
|
||||
"@types/semver": "^5.5.0",
|
||||
"@types/tmp": "0.0.33",
|
||||
"chokidar": "^2.0.4",
|
||||
"electron": "3.0.1",
|
||||
"enzyme": "^3.6.0",
|
||||
"enzyme-adapter-react-16": "^1.5.0",
|
||||
"enzyme-to-json": "^3.3.4",
|
||||
"jest": "^23.6.0",
|
||||
"jest-fetch-mock": "^1.6.6",
|
||||
"less": "^3.8.1",
|
||||
"log-symbols": "^2.2.0",
|
||||
"monaco-editor": "^0.14.3",
|
||||
"node-abi": "^2.4.4",
|
||||
"node-fetch": "^2.1.2",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"parcel-bundler": "^1.9.7",
|
||||
"react-test-renderer": "^16.4.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"@types/react": "^16.8.17",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/semver": "^6.0.0",
|
||||
"@types/tmp": "0.1.0",
|
||||
"chokidar": "^2.1.6",
|
||||
"coveralls": "^3.0.4",
|
||||
"electron": "5.0.6",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"jest": "^24.8.0",
|
||||
"jest-fetch-mock": "^2.1.2",
|
||||
"less": "^3.9.0",
|
||||
"log-symbols": "^3.0.0",
|
||||
"node-abi": "^2.9.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"parcel-bundler": "^1.12.3",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
"rimraf": "^2.6.3",
|
||||
"standard": "^12.0.1",
|
||||
"stylelint": "^9.6.0",
|
||||
"stylelint-config-standard": "^18.2.0",
|
||||
"ts-jest": "^23.10.2",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-microsoft-contrib": "^5.2.1",
|
||||
"tslint-react": "^3.6.0",
|
||||
"typescript": "^3.1.1"
|
||||
"stylelint": "^10.1.0",
|
||||
"stylelint-config-standard": "^18.3.0",
|
||||
"ts-jest": "^24.0.2",
|
||||
"tslint": "^5.18.0",
|
||||
"tslint-microsoft-contrib": "^6.2.0",
|
||||
"tslint-react": "^4.0.0",
|
||||
"typescript": "^3.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as electronModule from 'electron';
|
||||
import * as path from 'path';
|
||||
|
||||
const electron = electronModule.remote
|
||||
? electronModule.remote
|
||||
: electronModule;
|
||||
|
||||
export const USER_DATA_PATH = electron.app.getPath('userData');
|
||||
export const INDEX_HTML_NAME = 'index.html';
|
||||
export const MAIN_JS_NAME = 'main.js';
|
||||
export const RENDERER_JS_NAME = 'renderer.js';
|
||||
export const PACKAGE_NAME = 'package.json';
|
||||
|
||||
export const CONFIG_PATH = path.join(electron.app.getPath('home'), '.electron-fiddle');
|
||||
@@ -1,7 +1,5 @@
|
||||
export const main = `// Modules to control application life and create native browser window
|
||||
const {app, BrowserWindow} = require('electron')
|
||||
const path = require('path')
|
||||
const url = require('url')
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
@@ -9,7 +7,13 @@ let mainWindow
|
||||
|
||||
function createWindow () {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({width: 800, height: 600})
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
})
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile('index.html')
|
||||
|
||||
@@ -14,18 +14,10 @@ export enum ElectronVersionSource {
|
||||
remote = 'remote',
|
||||
local = 'local'
|
||||
}
|
||||
|
||||
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 interface NpmVersion {
|
||||
version: string;
|
||||
name?: string;
|
||||
localPath?: string;
|
||||
}
|
||||
|
||||
export interface EditorValues {
|
||||
@@ -35,11 +27,17 @@ export interface EditorValues {
|
||||
package?: string;
|
||||
}
|
||||
|
||||
export interface ElectronVersion extends GitHubVersion {
|
||||
export interface ElectronVersion extends NpmVersion {
|
||||
state: ElectronVersionState;
|
||||
source: ElectronVersionSource;
|
||||
}
|
||||
|
||||
export interface SetFiddleOptions {
|
||||
values: EditorValues;
|
||||
filePath?: string;
|
||||
templateName?: string;
|
||||
}
|
||||
|
||||
export interface OutputEntry {
|
||||
text: string;
|
||||
timestamp: number;
|
||||
@@ -51,11 +49,37 @@ export interface OutputOptions {
|
||||
isNotPre?: boolean;
|
||||
}
|
||||
|
||||
export interface WarningDialogTexts {
|
||||
ok?: string;
|
||||
cancel?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Templates {
|
||||
[index: string]: string | Templates;
|
||||
}
|
||||
|
||||
export type EditorId = 'main' | 'renderer' | 'html';
|
||||
// Editors
|
||||
export const enum EditorId {
|
||||
'main' = 'main',
|
||||
'renderer' = 'renderer',
|
||||
'html' = 'html'
|
||||
}
|
||||
|
||||
// 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.html ];
|
||||
export const ALL_PANELS = [ PanelId.docsDemo ];
|
||||
export const ALL_MOSAICS = [ ...ALL_EDITORS, ...ALL_PANELS ];
|
||||
|
||||
export type ArrowPosition = 'top' | 'left' | 'bottom' | 'right';
|
||||
|
||||
export const enum DocsDemoPage {
|
||||
DEFAULT = 'DEFAULT',
|
||||
DEMO_APP = 'DEMO_APP'
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
export enum IpcEvents {
|
||||
OPEN_SETTINGS = 'OPEN_SETTINGS',
|
||||
LOAD_GIST_REQUEST = 'LOAD_GIST_REQUEST',
|
||||
LOAD_ELECTRON_EXAMPLE_REQUEST = 'LOAD_ELECTRON_EXAMPLE_REQUEST',
|
||||
FIDDLE_RUN = 'FIDDLE_RUN',
|
||||
FIDDLE_PACKAGE = 'FIDDLE_PACKAGE',
|
||||
FIDDLE_MAKE = 'FIDDLE_MAKE',
|
||||
MONACO_EXECUTE_COMMAND = 'MONACO_EXECUTE_COMMAND',
|
||||
MONACO_TOGGLE_OPTION = 'MONACO_TOGGLE_OPTION',
|
||||
FS_NEW_FIDDLE = 'FS_NEW_FIDDLE',
|
||||
FS_OPEN_FIDDLE = 'FS_OPEN_FIDDLE',
|
||||
FS_OPEN_TEMPLATE = 'FS_OPEN_TEMPLATE',
|
||||
@@ -15,24 +17,27 @@ export enum IpcEvents {
|
||||
FS_SAVE_FIDDLE_DIALOG = 'FS_SAVE_FIDDLE_DIALOG',
|
||||
FS_SAVE_FIDDLE_ERROR = 'FS_SAVE_FIDDLE_ERROR',
|
||||
SHOW_WARNING_DIALOG = 'SHOW_WARNING_DIALOG',
|
||||
SHOW_CONFIRMATION_DIALOG = 'SHOW_CONFIRMATION_DIALOG',
|
||||
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR',
|
||||
TOGGLE_SOFT_WRAP = 'TOGGLE_SOFT_WRAP',
|
||||
TOGGLE_MINI_MAP = 'TOGGLE_MINI_MAP'
|
||||
CLEAR_CONSOLE = 'CLEAR_CONSOLE'
|
||||
}
|
||||
|
||||
export const ipcMainEvents = [
|
||||
IpcEvents.FS_SAVE_FIDDLE_DIALOG,
|
||||
IpcEvents.FS_SAVE_FIDDLE,
|
||||
IpcEvents.SHOW_WARNING_DIALOG
|
||||
IpcEvents.SHOW_WARNING_DIALOG,
|
||||
IpcEvents.SHOW_CONFIRMATION_DIALOG
|
||||
];
|
||||
|
||||
export const ipcRendererEvents = [
|
||||
IpcEvents.OPEN_SETTINGS,
|
||||
IpcEvents.LOAD_GIST_REQUEST,
|
||||
IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST,
|
||||
IpcEvents.FIDDLE_RUN,
|
||||
IpcEvents.FIDDLE_PACKAGE,
|
||||
IpcEvents.FIDDLE_MAKE,
|
||||
IpcEvents.MONACO_EXECUTE_COMMAND,
|
||||
IpcEvents.MONACO_TOGGLE_OPTION,
|
||||
IpcEvents.FS_NEW_FIDDLE,
|
||||
IpcEvents.FS_OPEN_FIDDLE,
|
||||
IpcEvents.FS_OPEN_TEMPLATE,
|
||||
@@ -41,6 +46,7 @@ export const ipcRendererEvents = [
|
||||
IpcEvents.FS_SAVE_FIDDLE_FORGE,
|
||||
IpcEvents.FS_SAVE_FIDDLE_GIST,
|
||||
IpcEvents.SHOW_WELCOME_TOUR,
|
||||
IpcEvents.TOGGLE_SOFT_WRAP,
|
||||
IpcEvents.TOGGLE_MINI_MAP
|
||||
IpcEvents.CLEAR_CONSOLE
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
@import "variables.less";
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 10px;
|
||||
|
||||
button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.address-bar {
|
||||
height: 28px;
|
||||
width: 360px;
|
||||
transition: width 0.4s;
|
||||
display: flex;
|
||||
|
||||
input,
|
||||
span {
|
||||
font-family: @fonts-common;
|
||||
font-weight: 300;
|
||||
font-size: 11px;
|
||||
cursor: auto;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-grow: 1;
|
||||
background: white;
|
||||
padding-left: 1rem;
|
||||
height: 2.6rem;
|
||||
}
|
||||
|
||||
input:invalid {
|
||||
box-shadow: inset 0 0 0 1px #e4252d;
|
||||
}
|
||||
|
||||
span {
|
||||
background: white;
|
||||
color: #1d1d1d;
|
||||
border-bottom-color: rgb(209, 209, 209);
|
||||
border-bottom-style: solid;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-width: 1px;
|
||||
border-image-outset: 0;
|
||||
border-image-repeat: stretch;
|
||||
border-image-slice: 100%;
|
||||
border-image-source: none;
|
||||
border-image-width: 1;
|
||||
border-left-color: rgb(209, 209, 209);
|
||||
border-left-style: solid;
|
||||
border-left-width: 1px;
|
||||
border-top-color: rgb(209, 209, 209);
|
||||
border-top-left-radius: 4px;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
display: inline-block;
|
||||
height: 1.2rem;
|
||||
letter-spacing: normal;
|
||||
margin: 0;
|
||||
padding: 6px 0 10px 10px;
|
||||
user-select: text;
|
||||
width: 132px;
|
||||
text-rendering: auto;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
span,
|
||||
input {
|
||||
border-color: @foreground-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@
|
||||
background-color: @background-1;
|
||||
border-bottom: @border;
|
||||
-webkit-app-region: drag;
|
||||
height: 23px;
|
||||
height: 24px;
|
||||
font-size: 1.4rem;
|
||||
line-height: 15px;
|
||||
|
||||
small,
|
||||
p,
|
||||
@@ -0,0 +1,3 @@
|
||||
button.version-chooser.bp3-button {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
@@ -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);
|
||||
//border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "variables.less";
|
||||
@import "../variables.less";
|
||||
|
||||
.dialogs {
|
||||
z-index: 10;
|
||||
@@ -10,10 +10,6 @@
|
||||
* {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.generateTokenText svg {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.darwin {
|
||||
@@ -171,25 +167,6 @@
|
||||
&:focus {
|
||||
border-color: @foreground-2;
|
||||
}
|
||||
|
||||
&.hasError {
|
||||
border-color: #df3434;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tokenDialog {
|
||||
.tokenSpinner {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,14 +183,13 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.add-version-dialog {
|
||||
label {
|
||||
white-space: normal;
|
||||
height: unset;
|
||||
.dialog-add-version {
|
||||
.bp3-file-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
.bp3-callout {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
.mosaic-tile {
|
||||
animation: fadein 0.15s;
|
||||
}
|
||||
|
||||
.mosaic-window-toolbar {
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 5px 0 5px;
|
||||
|
||||
.mosaic-controls {
|
||||
transform: scale(0.75);
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
@import "variables.less";
|
||||
@import "../variables.less";
|
||||
|
||||
.fiddle {
|
||||
.mosaic-window-toolbar.draggable {
|
||||
background: var(--background-2);
|
||||
.mosaic-window-toolbar {
|
||||
background: var(--background-1);
|
||||
box-shadow: inset 0 0 100px 100px rgba(53, 53, 53, 0.1);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@@ -34,16 +34,16 @@
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
font-size: 12px;
|
||||
overflow: visible;
|
||||
z-index: 4;
|
||||
background-color: @background-4;
|
||||
}
|
||||
|
||||
.mosaic {
|
||||
height: 100vh;
|
||||
|
||||
&.mosaic-blueprint-theme {
|
||||
background: @background-2;
|
||||
background: #bdbdbd15;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "variables.less";
|
||||
@import "../variables.less";
|
||||
|
||||
.output {
|
||||
font-size: 12px;
|
||||
@@ -7,21 +7,15 @@
|
||||
color: var(--text-color-1);
|
||||
background: var(--background-2);
|
||||
box-shadow: inset 0 0 100px 100px rgba(0, 0, 0, 0.15);
|
||||
border-top: @border;
|
||||
top: 10vh;
|
||||
padding: 0;
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
height: 0;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
transition: height 0.2s;
|
||||
transition: padding 0.2s;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&.showing {
|
||||
border-top: @border;
|
||||
padding-top: 10px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
white-space: pre;
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "variables.less";
|
||||
@import "../variables.less";
|
||||
|
||||
.settings {
|
||||
background: @background-3;
|
||||
@@ -35,21 +35,22 @@
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.electron-versions-table {
|
||||
button {
|
||||
margin: 0;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.action {
|
||||
text-align: center;
|
||||
width: 250px;
|
||||
}
|
||||
.bp3-divider {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.settings-appearance {
|
||||
button {
|
||||
margin-left: 0.5rem;
|
||||
.bp3-callout {
|
||||
max-width: 850px;
|
||||
}
|
||||
|
||||
.electron-versions-table {
|
||||
width: 100%;
|
||||
|
||||
.action {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,19 +174,16 @@
|
||||
}
|
||||
|
||||
li {
|
||||
padding-bottom: 5px;
|
||||
font-size: 1rem;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: @border;
|
||||
color: var(--text-color-1);
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: @foreground-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.show-me-panel {
|
||||
padding: 10px;
|
||||
overflow: scroll;
|
||||
|
||||
& > *:first-child {
|
||||
margin-top: 0;
|
||||
};
|
||||
|
||||
.bp3-callout {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.show-me-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
max-height: 400px;
|
||||
}
|
||||
@@ -6,3 +6,7 @@
|
||||
width: 100vw;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.tour-portal {
|
||||
z-index: 1000;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
button,
|
||||
input[type='button'],
|
||||
input[type='reset'],
|
||||
input[type='submit'],
|
||||
.force-button {
|
||||
background-color: @foreground-2;
|
||||
border: @border;
|
||||
border-radius: .4rem;
|
||||
cursor: pointer;
|
||||
color: @button-text-color;
|
||||
display: inline-block;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
height: 3rem;
|
||||
letter-spacing: .1rem;
|
||||
padding: 0 3.0rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
line-height: 2.95rem;
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 0 100px 100px rgba(255, 255, 255, 0.15);
|
||||
background-color: @foreground-1;
|
||||
border-color: @foreground-1;
|
||||
outline: 0;
|
||||
color: initial;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: default;
|
||||
opacity: .5;
|
||||
border-color: @foreground-3;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: @foreground-3;
|
||||
background: unset;
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&.button-outline {
|
||||
background-color: transparent;
|
||||
color: @foreground-2;
|
||||
border: @border;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border-color: @foreground-1;
|
||||
color: @foreground-1;
|
||||
}
|
||||
}
|
||||
|
||||
&.button-clear {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: @foreground-2;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: @foreground-1;;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: @foreground-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// A lot of these styles are based on Milligram, designed by
|
||||
// CJ Patolio (https://cjpatoilo.com/). While it's *almost*
|
||||
// perfect for Fiddle, a few of its styles are targeted at
|
||||
// websites.
|
||||
//
|
||||
// Check the framework out at https://github.com/milligram/milligram
|
||||
//
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright © 2017 CJ Patoilo <cjpatoilo@gmail.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 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.
|
||||
|
||||
@import "typography.less";
|
||||
@import "table.less";
|
||||
@import "form.less";
|
||||
@import "links.less";
|
||||
@import "button.less";
|
||||
@import "divider.less";
|
||||
@import "spacing.less";
|
||||
@@ -1,7 +0,0 @@
|
||||
@import "../variables.less";
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: @border;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
@import "../variables.less";
|
||||
|
||||
input[type='email'],
|
||||
input[type='number'],
|
||||
input[type='password'],
|
||||
input[type='search'],
|
||||
input[type='tel'],
|
||||
input[type='text'],
|
||||
input[type='url'],
|
||||
input[type='color'],
|
||||
input:not([type]),
|
||||
textarea,
|
||||
select {
|
||||
background-color: transparent;
|
||||
border: .1rem solid rgb(209, 209, 209);;
|
||||
border-radius: .4rem;
|
||||
box-shadow: none;
|
||||
box-sizing: inherit; // Forced to replace inherit values of the normalize.css
|
||||
height: 3.8rem;
|
||||
width: ~"calc(100% - 1rem)";
|
||||
padding-left: 1rem;
|
||||
|
||||
&:focus {
|
||||
border-color: @foreground-2;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
padding-right: 3.0rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 6.5rem;
|
||||
}
|
||||
|
||||
label,
|
||||
legend {
|
||||
display: block;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.label-inline {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
@import "../variables.less";
|
||||
|
||||
a {
|
||||
color: @foreground-2;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
.button,
|
||||
button,
|
||||
dd,
|
||||
dt,
|
||||
li {
|
||||
margin-bottom: 1.0rem;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
form,
|
||||
ol,
|
||||
p,
|
||||
pre,
|
||||
table,
|
||||
ul {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
@import "../variables.less";
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border-bottom: @border;
|
||||
padding: 1.2rem 1.5rem;
|
||||
text-align: left;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 300;
|
||||
letter-spacing: -.1rem;
|
||||
margin-bottom: 2.0rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4.6rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3.6rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2.8rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: -.08rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.8rem;
|
||||
letter-spacing: -.05rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.mosaic-tile {
|
||||
animation: fadein 0.15s;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: @background-2;
|
||||
background: @background-1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
font-family: @fonts-common;
|
||||
color: @text-color-1;
|
||||
font-size: 1.6em;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.6;
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fiddle {
|
||||
@@ -35,12 +33,6 @@ body {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: @border;
|
||||
background-color: @background-1;
|
||||
-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);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
@import "variables.less";
|
||||
|
||||
input:focus,
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
pre.prettyprint .lit {
|
||||
color: @foreground-2;
|
||||
}
|
||||
|
||||
.share-icon {
|
||||
background-color: @foreground-2;
|
||||
color: @text-color-1;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f4f5f6;
|
||||
border-left: 0.3rem solid @foreground-2;
|
||||
|
||||
&.prettyprint {
|
||||
border-left: 0.3rem solid @foreground-2 !important;
|
||||
color: #655d5d;
|
||||
|
||||
.tag {
|
||||
color: @foreground-2;
|
||||
}
|
||||
|
||||
.atn {
|
||||
color: #0065ce;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
// Core Framework
|
||||
@import "core/core.less";
|
||||
// Blueprint
|
||||
@import "blueprint.less";
|
||||
|
||||
// Variables, also imported in each file
|
||||
@import "variables.less";
|
||||
|
||||
// Components
|
||||
@import "main.less";
|
||||
@import "container.less";
|
||||
@import "commands.less";
|
||||
@import "output.less";
|
||||
@import "dialogs.less";
|
||||
@import "mosaic-vendor.less";
|
||||
@import "mosaic.less";
|
||||
@import "settings.less";
|
||||
@import "chrome-mac.less";
|
||||
@import "editors.less";
|
||||
@import "tour.less";
|
||||
|
||||
// Resets
|
||||
@import "resets.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/commands-version-chooser.less";
|
||||
@import "components/show-me.less";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@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);
|
||||
|
||||
@@ -15,6 +15,11 @@ export function getRunItems(): Array<MenuItemConstructorOptions> {
|
||||
label: 'Run Fiddle',
|
||||
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
|
||||
},
|
||||
{
|
||||
id: 'clear_console',
|
||||
label: 'Clear Console',
|
||||
click: () => ipcMainManager.send(IpcEvents.CLEAR_CONSOLE)
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
}
|
||||
@@ -136,17 +141,17 @@ export function createContextMenu(browserWindow: BrowserWindow) {
|
||||
{
|
||||
id: 'cut',
|
||||
label: 'Cut',
|
||||
role: editFlags.canCut ? 'cut' : '',
|
||||
role: 'cut',
|
||||
enabled: editFlags.canCut
|
||||
}, {
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
role: editFlags.canCopy ? 'copy' : '',
|
||||
role: 'copy',
|
||||
enabled: editFlags.canCopy
|
||||
}, {
|
||||
id: 'paste',
|
||||
label: 'Paste',
|
||||
role: editFlags.canPaste ? 'paste' : '',
|
||||
role: 'paste',
|
||||
enabled: editFlags.canPaste
|
||||
}, {
|
||||
type: 'separator'
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
|
||||
/**
|
||||
* Installs developer tools if we're in dev mode.
|
||||
*
|
||||
* @export
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function setupDevTools(): Promise<void> {
|
||||
if (!isDevMode) return;
|
||||
|
||||
const {
|
||||
default: installExtension,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
REACT_PERF
|
||||
} = require('electron-devtools-installer');
|
||||
|
||||
try {
|
||||
const react = await installExtension(REACT_DEVELOPER_TOOLS);
|
||||
console.log(`installDevTools: Installed ${react}`);
|
||||
|
||||
const perf = await installExtension(REACT_PERF);
|
||||
console.log(`installDevTools: Installed ${perf}`);
|
||||
} catch (error) {
|
||||
console.warn(`installDevTools: Error occured:`, error);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ export function setupDialogs() {
|
||||
ipcMainManager.on(IpcEvents.SHOW_WARNING_DIALOG, (_event, args) => {
|
||||
showWarningDialog(args);
|
||||
});
|
||||
|
||||
ipcMainManager.on(IpcEvents.SHOW_CONFIRMATION_DIALOG, (_event, args) => {
|
||||
showConfirmationDialog(args);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,3 +30,15 @@ function showWarningDialog(args: Electron.MessageBoxOptions) {
|
||||
...args
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog
|
||||
*
|
||||
* @param {Electron.MessageBoxOptions} args
|
||||
*/
|
||||
function showConfirmationDialog(args: Electron.MessageBoxOptions) {
|
||||
dialog.showMessageBox(getOrCreateMainWindow(), {
|
||||
type: 'warning',
|
||||
...args
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,15 +2,17 @@ import { dialog } from 'electron';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../constants';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../shared-constants';
|
||||
import { ipcMainManager } from './ipc';
|
||||
|
||||
/**
|
||||
* Ensures that we're listening to file events
|
||||
*/
|
||||
export function setupFileListeners() {
|
||||
ipcMainManager.on(IpcEvents.FS_SAVE_FIDDLE_DIALOG, () => showSaveDialog());
|
||||
ipcMainManager.on(IpcEvents.FS_SAVE_FIDDLE_DIALOG, () => {
|
||||
showSaveDialog();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 function onFirstRunMaybe() {
|
||||
if (isFirstRun()) {
|
||||
promptMoveToApplicationsFolder();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user if the app should be moved to the
|
||||
* applications folder.
|
||||
*/
|
||||
function promptMoveToApplicationsFolder(): void {
|
||||
if (process.platform !== 'darwin') return;
|
||||
if (isDevMode() || app.isInApplicationsFolder()) return;
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ['Move to Applications Folder', 'Do Not Move'],
|
||||
defaultId: 0,
|
||||
message: 'Move to Applications Folder?',
|
||||
}, (response) => {
|
||||
if (response === 0) {
|
||||
app.moveToApplicationsFolder();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { EventEmitter } from 'events';
|
||||
import { IpcEvents, ipcMainEvents } from '../ipc-events';
|
||||
import { IpcEvents, ipcMainEvents, WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
|
||||
import { getOrCreateMainWindow } from './windows';
|
||||
|
||||
/**
|
||||
@@ -12,12 +12,25 @@ import { getOrCreateMainWindow } from './windows';
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
export class IpcMainManager extends EventEmitter {
|
||||
public readyWebContents = new WeakSet<Electron.WebContents>();
|
||||
private messageQueue = new WeakMap<Electron.WebContents, Array<[IpcEvents, Array<any> | undefined]>>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
ipcMainEvents.forEach((name) => {
|
||||
ipcMain.on(name, (...args: Array<any>) => this.emit(name, ...args));
|
||||
});
|
||||
|
||||
ipcMain.on(WEBCONTENTS_READY_FOR_IPC_SIGNAL, (event: Electron.Event) => {
|
||||
this.readyWebContents.add(event.sender);
|
||||
const queue = this.messageQueue.get(event.sender);
|
||||
this.messageQueue.delete(event.sender);
|
||||
if (!queue) return;
|
||||
for (const item of queue) {
|
||||
this.send(item[0], item[1], event.sender);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +44,11 @@ export class IpcMainManager extends EventEmitter {
|
||||
public send(channel: IpcEvents, args?: Array<any>, target?: Electron.WebContents) {
|
||||
const _target = target || getOrCreateMainWindow().webContents;
|
||||
const _args = args || [];
|
||||
if (!this.readyWebContents.has(_target)) {
|
||||
const existing = this.messageQueue.get(_target) || [];
|
||||
this.messageQueue.set(_target, [...existing, [channel, args]]);
|
||||
return;
|
||||
}
|
||||
|
||||
_target.send(channel, ..._args);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
import { setupAboutPanel } from '../utils/set-about-panel';
|
||||
import { setupDevTools } from './devtools';
|
||||
import { setupDialogs } from './dialogs';
|
||||
import { onFirstRunMaybe } from './first-run';
|
||||
import { listenForProtocolHandler, setupProtocolHandler } from './protocol';
|
||||
import { shouldQuit } from './squirrel';
|
||||
import { setupUpdates } from './update';
|
||||
import { getOrCreateMainWindow } from './windows';
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (require('electron-squirrel-startup')) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
app.setName('Electron Fiddle');
|
||||
|
||||
listenForProtocolHandler();
|
||||
|
||||
app.on('ready', async () => {
|
||||
// If we're packaged, we want to run
|
||||
// React in production mode.
|
||||
if (!isDevMode()) {
|
||||
process.env.NODE_ENV = 'production';
|
||||
}
|
||||
/**
|
||||
* Handle the app's "ready" event. This is essentially
|
||||
* the method that takes care of booting the application.
|
||||
*/
|
||||
export async function onReady() {
|
||||
onFirstRunMaybe();
|
||||
if (!isDevMode()) process.env.NODE_ENV = 'production';
|
||||
|
||||
getOrCreateMainWindow();
|
||||
setupAboutPanel();
|
||||
|
||||
const { setupMenu } = await import('./menu');
|
||||
const { setupFileListeners } = await import('./files');
|
||||
@@ -32,15 +29,55 @@ app.on('ready', async () => {
|
||||
setupFileListeners();
|
||||
setupUpdates();
|
||||
setupDialogs();
|
||||
});
|
||||
setupDevTools();
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
/**
|
||||
* Handle the "before-quit" event
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function onBeforeQuit() {
|
||||
(global as any).isQuitting = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* All windows have been closed, quit on anything but
|
||||
* macOS.
|
||||
*/
|
||||
export function onWindowsAllClosed() {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.on('activate', getOrCreateMainWindow);
|
||||
/**
|
||||
* The main method - and the first function to run
|
||||
* when Fiddle is launched.
|
||||
*
|
||||
* Exported for testing purposes.
|
||||
*/
|
||||
export function main() {
|
||||
// Handle creating/removing shortcuts on Windows when
|
||||
// installing/uninstalling.
|
||||
if (shouldQuit()) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the app's name
|
||||
app.setName('Electron Fiddle');
|
||||
|
||||
// Ensure that there's only ever one Fiddle running
|
||||
listenForProtocolHandler();
|
||||
|
||||
// Launch
|
||||
app.on('ready', onReady);
|
||||
app.on('before-quit', onBeforeQuit);
|
||||
app.on('window-all-closed', onWindowsAllClosed);
|
||||
app.on('activate', getOrCreateMainWindow);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -121,6 +121,7 @@ function getTasksMenu(): MenuItemConstructorOptions {
|
||||
const tasksMenu: Array<MenuItemConstructorOptions> = [
|
||||
{
|
||||
label: 'Run Fiddle...',
|
||||
accelerator: 'F5',
|
||||
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
|
||||
},
|
||||
{
|
||||
@@ -205,7 +206,7 @@ function getFileMenu(): MenuItemConstructorOptions {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Save to Gist',
|
||||
label: 'Publish to Gist',
|
||||
click: () => ipcMainManager.send(IpcEvents.FS_SAVE_FIDDLE_GIST),
|
||||
},
|
||||
{
|
||||
@@ -250,11 +251,11 @@ export function setupMenu() {
|
||||
item.submenu.push({ type: 'separator' }, { role: 'resetzoom' }, { role: 'zoomin' }, { role: 'zoomout' }); // Add zooming actions
|
||||
item.submenu.push({ type: 'separator' }, {
|
||||
label: 'Toggle Soft Wrap',
|
||||
click: () => ipcMainManager.send(IpcEvents.TOGGLE_SOFT_WRAP),
|
||||
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, [ 'wordWrap' ]),
|
||||
});
|
||||
item.submenu.push({ type: 'separator' }, {
|
||||
label: 'Toggle Mini Map',
|
||||
click: () => ipcMainManager.send(IpcEvents.TOGGLE_MINI_MAP),
|
||||
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, [ 'minimap.enabled' ]),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as path from 'path';
|
||||
import * as nodeUrl from 'url';
|
||||
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
import { ipcMainManager } from './ipc';
|
||||
|
||||
const PROTOCOL = 'electron-fiddle';
|
||||
@@ -14,21 +15,63 @@ const handlePotentialProtocolLaunch = (url: string) => {
|
||||
app.once('ready', () => handlePotentialProtocolLaunch(url));
|
||||
return;
|
||||
}
|
||||
const parsed = nodeUrl.parse(url.replace(/\/$/, ''));
|
||||
if (!parsed.pathname) return;
|
||||
const pathParts = parsed.pathname.split('/');
|
||||
if (pathParts.length <= 0 || pathParts.length > 2) return;
|
||||
|
||||
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [parsed.pathname]);
|
||||
const parsed = nodeUrl.parse(url.replace(/\/$/, ''));
|
||||
if (!parsed.pathname || !parsed.hostname) return;
|
||||
|
||||
const pathParts = parsed.pathname.split('/').slice(1);
|
||||
|
||||
switch (parsed.hostname) {
|
||||
// electron-fiddle://gist/blub
|
||||
case 'gist':
|
||||
if (pathParts.length === 1) {
|
||||
// We only have a gist ID
|
||||
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [{
|
||||
id: pathParts[0],
|
||||
}]);
|
||||
} else if (pathParts.length === 2) {
|
||||
// We have a gist owner and gist ID, we can ignore the owner
|
||||
ipcMainManager.send(IpcEvents.LOAD_GIST_REQUEST, [{
|
||||
id: pathParts[1],
|
||||
}]);
|
||||
} else {
|
||||
// This is a super invalid gist launch
|
||||
return;
|
||||
}
|
||||
break;
|
||||
// electron-fiddle://electron/{ref}/{path}
|
||||
case 'electron':
|
||||
if (pathParts.length > 1) {
|
||||
// First part of the commit HASH / ref / branch
|
||||
// Rest is the path to the example
|
||||
ipcMainManager.send(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, [{
|
||||
ref: pathParts[0],
|
||||
path: pathParts.slice(1).join('/'),
|
||||
}]);
|
||||
} else {
|
||||
// This is an invalid electron launch
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const scanArgv = (argv: Array<string>) => {
|
||||
const protocolArg = argv.find((arg) => arg.startsWith(`${PROTOCOL}://`));
|
||||
if (protocolArg) {
|
||||
console.info('Found protocol arg in argv:', protocolArg);
|
||||
handlePotentialProtocolLaunch(protocolArg);
|
||||
}
|
||||
};
|
||||
|
||||
export const scanNpmArgv = (argv: string) => {
|
||||
const parsedArgv = JSON.parse(argv);
|
||||
const { original: args } = parsedArgv;
|
||||
scanArgv(args);
|
||||
};
|
||||
|
||||
export const listenForProtocolHandler = () => {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) app.quit();
|
||||
@@ -39,7 +82,10 @@ export const listenForProtocolHandler = () => {
|
||||
}
|
||||
});
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// pass protocol URL via npm start args in dev mode
|
||||
if (isDevMode() && process.env.npm_config_argv) {
|
||||
scanNpmArgv(process.env.npm_config_argv);
|
||||
} else if (process.platform === 'win32') {
|
||||
scanArgv(process.argv);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function shouldQuit() {
|
||||
return require('electron-squirrel-startup');
|
||||
}
|
||||
@@ -5,7 +5,9 @@ export function setupUpdates() {
|
||||
// We delay this work by 10s to ensure that the
|
||||
// app doesn't have to worry about updating during launch
|
||||
setTimeout(() => {
|
||||
require('update-electron-app')({
|
||||
const updateApp = require('update-electron-app');
|
||||
|
||||
updateApp({
|
||||
repo: 'electron/fiddle',
|
||||
updateInterval: '1 hour',
|
||||
});
|
||||
|
||||
@@ -20,7 +20,8 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
|
||||
acceptFirstMouse: true,
|
||||
backgroundColor: '#1d2427',
|
||||
webPreferences: {
|
||||
webviewTag: false
|
||||
webviewTag: false,
|
||||
nodeIntegration: true
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -33,10 +34,13 @@ export function getMainWindowOptions(): Electron.BrowserWindowConstructorOptions
|
||||
* @returns {Electron.BrowserWindow}
|
||||
*/
|
||||
export function createMainWindow(): Electron.BrowserWindow {
|
||||
console.log(`Creating main window`);
|
||||
const browserWindow = new BrowserWindow(getMainWindowOptions());
|
||||
browserWindow.loadFile('./dist/index.html');
|
||||
browserWindow.loadFile('./dist/static/index.html');
|
||||
|
||||
browserWindow.webContents.once('dom-ready', () => {
|
||||
browserWindow.show();
|
||||
|
||||
if (browserWindow) {
|
||||
createContextMenu(browserWindow);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { when } from 'mobx';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
|
||||
import { EditorValues } from '../interfaces';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ALL_EDITORS, EditorId, EditorValues } 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 { isEditorBackup } from '../utils/type-checks';
|
||||
import { FileManager } from './file-manager';
|
||||
import { RemoteLoader } from './remote-loader';
|
||||
import { Runner } from './runner';
|
||||
import { appState } from './state';
|
||||
import { getTheme } from './themes';
|
||||
import { TouchBarManager } from './touch-bar-manager';
|
||||
|
||||
/**
|
||||
* The top-level class controlling the whole app. This is *not* a React component,
|
||||
@@ -18,44 +26,55 @@ export class App {
|
||||
public monaco: typeof MonacoType | null = null;
|
||||
public state = appState;
|
||||
public fileManager = new FileManager(appState);
|
||||
public remoteLoader = new RemoteLoader(appState);
|
||||
public runner = new Runner(appState);
|
||||
public touchBarManager: TouchBarManager | undefined;
|
||||
|
||||
constructor() {
|
||||
this.getValues = this.getValues.bind(this);
|
||||
this.setValues = this.setValues.bind(this);
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
this.touchBarManager = new TouchBarManager(appState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the values on all three editors.
|
||||
*
|
||||
* @param {EditorValues} values
|
||||
* @param {warn} warn - Should we warn before overwriting unsaved data?
|
||||
*/
|
||||
public async setValues(values: Partial<EditorValues>): Promise<boolean> {
|
||||
public async setValues(values: Partial<EditorValues>, warn: boolean = true): Promise<boolean> {
|
||||
const { ElectronFiddle: fiddle } = window;
|
||||
|
||||
if (!fiddle) {
|
||||
throw new Error('Fiddle not ready');
|
||||
}
|
||||
|
||||
if (appState.isUnsaved) {
|
||||
const isUserSure = confirm('Your current fiddle is unsaved. Are you sure you want to overwrite it?');
|
||||
if (!isUserSure) return false;
|
||||
if (this.state.isUnsaved && warn) {
|
||||
this.state.isWarningDialogShowing = true;
|
||||
await when(() => !this.state.isWarningDialogShowing);
|
||||
|
||||
if (!this.state.warningDialogLastResult) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const { main, html, renderer } = fiddle.editors;
|
||||
for (const name of ALL_EDITORS) {
|
||||
const editor = fiddle.editors[name];
|
||||
const backup = this.state.closedPanels[name];
|
||||
|
||||
if (html && html.setValue && values.html) {
|
||||
html.setValue(values.html);
|
||||
if (isEditorBackup(backup) && backup.model && values[name]) {
|
||||
// The editor does not exist, attempt to set it on the backup
|
||||
backup.model.setValue(values[name]!);
|
||||
} else if (editor && editor.setValue && values[name]) {
|
||||
// The editor exists, set the value directly
|
||||
editor.setValue(values[name]!);
|
||||
}
|
||||
}
|
||||
|
||||
if (main && main.setValue && values.main) {
|
||||
main.setValue(values.main);
|
||||
}
|
||||
|
||||
if (renderer && renderer.setValue && values.renderer) {
|
||||
renderer.setValue(values.renderer);
|
||||
}
|
||||
|
||||
appState.isUnsaved = false;
|
||||
this.state.isUnsaved = false;
|
||||
this.setupUnsavedOnChangeListener();
|
||||
|
||||
return true;
|
||||
@@ -73,15 +92,14 @@ export class App {
|
||||
throw new Error('Fiddle not ready');
|
||||
}
|
||||
|
||||
const { main, html, renderer } = fiddle.editors;
|
||||
const values: EditorValues = {
|
||||
html: html && html.getValue() ? html.getValue() : '',
|
||||
main: main && main.getValue() ? main.getValue() : '',
|
||||
renderer: renderer && renderer.getValue() ? renderer.getValue() : '',
|
||||
html: getEditorValue(EditorId.html),
|
||||
main: getEditorValue(EditorId.main),
|
||||
renderer: getEditorValue(EditorId.renderer),
|
||||
};
|
||||
|
||||
if (options && options.include !== false) {
|
||||
values.package = await getPackageJson(appState, values, options);
|
||||
values.package = await getPackageJson(this.state, values, options);
|
||||
}
|
||||
|
||||
return values;
|
||||
@@ -91,33 +109,37 @@ export class App {
|
||||
* Initial setup call, loading Monaco and kicking off the React
|
||||
* render process.
|
||||
*/
|
||||
public async setup(): Promise<void> {
|
||||
public async setup(): Promise<void | Element | React.Component> {
|
||||
this.setupTheme();
|
||||
|
||||
const React = await import('react');
|
||||
const { render } = await import('react-dom');
|
||||
const { Header } = await import('./components/header');
|
||||
const { Dialogs } = await import('./components/dialogs');
|
||||
const { Editors } = await import('./components/editors');
|
||||
const { OutputEditorsWrapper } = await import('./components/output-editors-wrapper');
|
||||
const { Header } = await import('./components/header');
|
||||
|
||||
const className = `${process.platform} container`;
|
||||
const app = (
|
||||
<div className={className}>
|
||||
<Header appState={appState} />
|
||||
<Dialogs appState={appState} />
|
||||
<Editors appState={appState} />
|
||||
<Dialogs appState={this.state} />
|
||||
<Header appState={this.state} />
|
||||
<OutputEditorsWrapper appState={this.state} />
|
||||
</div>
|
||||
);
|
||||
|
||||
render(app, document.getElementById('app'));
|
||||
const rendered = render(app, document.getElementById('app'));
|
||||
|
||||
this.setupResizeListener();
|
||||
|
||||
ipcRenderer.send(WEBCONTENTS_READY_FOR_IPC_SIGNAL);
|
||||
|
||||
// Todo: A timer here is terrible. Let's fix this
|
||||
// and ensure we actually do it once Editors have mounted.
|
||||
setTimeout(() => {
|
||||
this.setupUnsavedOnChangeListener();
|
||||
}, 1500);
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +151,7 @@ export class App {
|
||||
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
|
||||
const editor = window.ElectronFiddle.editors[key];
|
||||
const disposable = editor.onDidChangeModelContent(() => {
|
||||
appState.isUnsaved = true;
|
||||
this.state.isUnsaved = true;
|
||||
disposable.dispose();
|
||||
});
|
||||
});
|
||||
@@ -142,11 +164,17 @@ export class App {
|
||||
*/
|
||||
public async setupTheme(): Promise<void> {
|
||||
const tag: HTMLStyleElement | null = document.querySelector('style#fiddle-theme');
|
||||
const theme = await getTheme(appState.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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,10 +186,7 @@ export class App {
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
if (!process.env.TEST && !process.env.JEST_WORKER_ID) {
|
||||
window.ElectronFiddle.contentChangeListeners = [];
|
||||
window.ElectronFiddle.app = new App();
|
||||
window.ElectronFiddle.app.setup()
|
||||
.catch((error) => console.error(error));
|
||||
}
|
||||
window.ElectronFiddle = window.ElectronFiddle || {};
|
||||
window.ElectronFiddle.contentChangeListeners = window.ElectronFiddle.contentChangeListeners || [];
|
||||
window.ElectronFiddle.app = window.ElectronFiddle.app || new App();
|
||||
window.ElectronFiddle.app.setup();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as fsType from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { USER_DATA_PATH } from '../constants';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { normalizeVersion } from '../utils/normalize-version';
|
||||
import { USER_DATA_PATH } from './constants';
|
||||
|
||||
/**
|
||||
* The binary manager takes care of downloading Electron versions
|
||||
@@ -78,9 +78,12 @@ export class BinaryManager {
|
||||
|
||||
const zipPath = await eDownload({ version });
|
||||
const extractPath = this.getDownloadPath(version);
|
||||
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking`);
|
||||
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking to ${extractPath}`);
|
||||
|
||||
try {
|
||||
// Ensure the target path is empty
|
||||
await fs.emptyDir(extractPath);
|
||||
|
||||
const electronFiles = await this.unzip(zipPath, extractPath);
|
||||
console.log(`Unzipped ${version}`, electronFiles);
|
||||
} catch (error) {
|
||||
@@ -153,6 +156,7 @@ export class BinaryManager {
|
||||
public async getIsDownloaded(version: string, dir?: string): Promise<boolean> {
|
||||
const expectedPath = this.getElectronBinaryPath(version, dir);
|
||||
const fs = await fancyImport<typeof fsType>('fs-extra');
|
||||
|
||||
return fs.existsSync(expectedPath);
|
||||
}
|
||||
|
||||
@@ -167,7 +171,7 @@ export class BinaryManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzips an electron package so that we can actaully use it.
|
||||
* Unzips an electron package so that we can actually use it.
|
||||
*
|
||||
* @param {string} zipPath
|
||||
* @param {string} extractPath
|
||||
@@ -179,7 +183,11 @@ export class BinaryManager {
|
||||
|
||||
process.noAsar = true;
|
||||
|
||||
extract(zipPath, { dir: extractPath }, (error: Error) => {
|
||||
const options = {
|
||||
dir: extractPath,
|
||||
};
|
||||
|
||||
extract(zipPath, options, (error: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import { reaction } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../constants';
|
||||
import { classNames } from '../../utils/classnames';
|
||||
import { getTitle } from '../../utils/get-title';
|
||||
import { idFromUrl } from '../../utils/gist';
|
||||
import { getOctokit } from '../../utils/octokit';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface AddressBarProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface AddressBarState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AddressBar extends React.Component<AddressBarProps, AddressBarState> {
|
||||
constructor(props: AddressBarProps) {
|
||||
super(props);
|
||||
|
||||
this.loadFiddle = this.loadFiddle.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
value: this.props.appState.gistId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the form's submit event, trying to load whatever
|
||||
* URL was entered.
|
||||
*
|
||||
* @param {React.SyntheticEvent<HTMLFormElement>} event
|
||||
*/
|
||||
public async handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.appState.gistId = idFromUrl(this.state.value) || this.state.value;
|
||||
|
||||
console.log('Loading');
|
||||
|
||||
if (this.state.value) {
|
||||
this.loadFiddle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the component mounts, we'll subscribe to gistId changes
|
||||
*/
|
||||
public componentDidMount() {
|
||||
reaction(
|
||||
() => this.props.appState.gistId,
|
||||
(gistId: string) => this.setState({ value: gistId })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles invalid input to the address bar
|
||||
*
|
||||
* @param {React.InvalidEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleInvalid(event: React.InvalidEvent<HTMLInputElement>) {
|
||||
event.target.setCustomValidity('URL should begin with https://gist.github.com');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a fiddle
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* @memberof AddressBar
|
||||
*/
|
||||
public async loadFiddle(): Promise<boolean> {
|
||||
const { appState } = this.props;
|
||||
|
||||
if (!confirm('Are you sure you want to load a new fiddle, all current progress will be lost?')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const octo = await getOctokit();
|
||||
const gist = await octo.gists.get({
|
||||
gist_id: appState.gistId
|
||||
});
|
||||
|
||||
await window.ElectronFiddle.app.setValues({
|
||||
html: gist.data.files[INDEX_HTML_NAME].content,
|
||||
main: gist.data.files[MAIN_JS_NAME].content,
|
||||
renderer: gist.data.files[RENDERER_JS_NAME].content,
|
||||
});
|
||||
|
||||
document.title = getTitle(appState);
|
||||
appState.localPath = null;
|
||||
} catch (error) {
|
||||
console.warn(`Loading Fiddle failed`, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isUnsaved } = this.props.appState;
|
||||
const { value } = this.state;
|
||||
const isEmpty = /https:\/\/gist\.github\.com\/(.+)$/.test(value);
|
||||
const className = classNames('address-bar', isUnsaved, { empty: !isEmpty });
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
key='addressbar'
|
||||
pattern='https:\/\/gist\.github\.com\/(.+)$'
|
||||
placeholder='https://gist.github.com/...'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onInvalid={this.handleInvalid}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "8"
|
||||
|
||||
init:
|
||||
- git config --global core.symlinks true
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- node --version
|
||||
- npm ci
|
||||
|
||||
cache:
|
||||
- '%APPDATA%\npm-cache -> appveyor.yml'
|
||||
|
||||
test_script:
|
||||
- npm run lint
|
||||
- npm run test
|
||||
|
||||
build: off
|
||||
@@ -1,3 +1,4 @@
|
||||
import { remote } from 'electron';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
@@ -10,11 +11,25 @@ export interface ChromeMacProps {
|
||||
|
||||
@observer
|
||||
export class ChromeMac extends React.Component<ChromeMacProps> {
|
||||
public handleDoubleClick = () => {
|
||||
const doubleClickAction = remote.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
|
||||
const win = remote.getCurrentWindow();
|
||||
if (doubleClickAction === 'Minimize') {
|
||||
win.minimize();
|
||||
} else if (doubleClickAction === 'Maximize') {
|
||||
if (!win.isMaximized()) {
|
||||
win.maximize();
|
||||
} else {
|
||||
win.unmaximize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (process.platform !== 'darwin') return null;
|
||||
|
||||
return (
|
||||
<div className='chrome drag'>
|
||||
<div className='chrome drag' onDoubleClick={this.handleDoubleClick}>
|
||||
<small>{getTitle(this.props.appState)}</small>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Button, InputGroup, Intent } from '@blueprintjs/core';
|
||||
import classnames from 'classnames';
|
||||
import { reaction } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { idFromUrl, urlFromId } from '../../utils/gist';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface AddressBarProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface AddressBarState {
|
||||
value: string;
|
||||
loaders: {
|
||||
gist: any;
|
||||
example: any;
|
||||
};
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AddressBar extends React.Component<AddressBarProps, AddressBarState> {
|
||||
constructor(props: AddressBarProps) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.submit = this.submit.bind(this);
|
||||
|
||||
const { gistId } = this.props.appState;
|
||||
const value = gistId ? urlFromId(gistId) : '';
|
||||
|
||||
const { remoteLoader } = window.ElectronFiddle.app;
|
||||
|
||||
this.state = {
|
||||
value,
|
||||
loaders: {
|
||||
gist: remoteLoader.loadFiddleFromGist.bind(remoteLoader),
|
||||
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the form's submit event, trying to load whatever
|
||||
* URL was entered.
|
||||
*
|
||||
* @param {React.SyntheticEvent<HTMLFormElement>} event
|
||||
*/
|
||||
public handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the address bar's value to app state and load the fiddle.
|
||||
*
|
||||
* @memberof AddressBar
|
||||
*/
|
||||
public submit() {
|
||||
const { remoteLoader } = window.ElectronFiddle.app;
|
||||
if (this.state.value) {
|
||||
remoteLoader.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the component mounts, we'll subscribe to gistId changes
|
||||
*/
|
||||
public componentDidMount() {
|
||||
const { appState } = this.props;
|
||||
const { loaders } = this.state;
|
||||
reaction(
|
||||
() => appState.gistId,
|
||||
(gistId: string) => this.setState({ value: urlFromId(gistId) })
|
||||
);
|
||||
ipcRendererManager.on(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
|
||||
ipcRendererManager.on(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const { loaders } = this.state;
|
||||
ipcRendererManager.removeListener(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
|
||||
ipcRendererManager.removeListener(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the change event, which usually just updates the address bar's value
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.setState({ value: event.target.value });
|
||||
}
|
||||
|
||||
public renderLoadButton(isValueCorrect: boolean): JSX.Element {
|
||||
return (
|
||||
<Button
|
||||
disabled={!isValueCorrect}
|
||||
icon='cloud-download'
|
||||
text='Load Fiddle'
|
||||
onClick={this.submit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isUnsaved, isPublishing } = this.props.appState;
|
||||
const { value } = this.state;
|
||||
const isCorrect = /https:\/\/gist\.github\.com\/(.+)$/.test(value);
|
||||
const className = classnames('address-bar', isUnsaved, { empty: !value });
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={this.handleSubmit}>
|
||||
<fieldset disabled={isPublishing}>
|
||||
<InputGroup
|
||||
key='addressbar'
|
||||
leftIcon='geosearch'
|
||||
intent={isCorrect || !value ? undefined : Intent.DANGER}
|
||||
onChange={this.handleChange}
|
||||
placeholder='https://gist.github.com/...'
|
||||
value={value}
|
||||
rightElement={this.renderLoadButton(isCorrect)}
|
||||
/>
|
||||
</fieldset>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Button, Menu, MenuItem, Popover, Position } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ALL_EDITORS, MosaicId, PanelId } from '../../interfaces';
|
||||
import { getVisibleMosaics } from '../../utils/editors-mosaic-arrangement';
|
||||
import { AppState } from '../state';
|
||||
import { TITLE_MAP } from './editors';
|
||||
|
||||
export interface EditorDropdownState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EditorDropdownProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dropdown allowing users to toggle the various editors
|
||||
*
|
||||
* @class EditorDropdown
|
||||
* @extends {React.Component<EditorDropdownProps, EditorDropdownState>}
|
||||
*/
|
||||
@observer
|
||||
export class EditorDropdown extends React.Component<EditorDropdownProps, EditorDropdownState> {
|
||||
constructor(props: EditorDropdownProps) {
|
||||
super(props);
|
||||
|
||||
this.onItemClick = this.onItemClick.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Popover content={this.renderMenu()} position={Position.BOTTOM}>
|
||||
<Button icon='applications' text='Editors' />
|
||||
</Popover>
|
||||
{this.renderDocsDemos()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderDocsDemos() {
|
||||
if (!process.env.FIDDLE_DOCS_DEMOS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon='help'
|
||||
text='Docs & Demos'
|
||||
id={PanelId.docsDemo}
|
||||
onClick={this.onItemClick}
|
||||
active={!this.props.appState.closedPanels.docsDemo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMenu() {
|
||||
return (
|
||||
<Menu>
|
||||
{...this.renderMenuItems()}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMenuItems() {
|
||||
const { appState } = this.props;
|
||||
const result: Array<JSX.Element> = [];
|
||||
const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
|
||||
|
||||
for (const id of ALL_EDITORS) {
|
||||
result.push(
|
||||
<MenuItem
|
||||
icon={visibleMosaics.includes(id) ? 'eye-open' : 'eye-off'}
|
||||
key={id}
|
||||
text={TITLE_MAP[id]}
|
||||
id={id}
|
||||
onClick={this.onItemClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public onItemClick(event: React.MouseEvent) {
|
||||
const { id } = event.currentTarget;
|
||||
const { appState } = this.props;
|
||||
const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
|
||||
|
||||
if (visibleMosaics.includes(id as MosaicId)) {
|
||||
console.log(`EditorDropdown: Closing ${id}`);
|
||||
appState.hideAndBackupMosaic(id as MosaicId);
|
||||
} else {
|
||||
console.log(`EditorDropdown: Opening ${id}`);
|
||||
appState.showMosaic(id as MosaicId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import { faSpinner, faUpload } from '@fortawesome/fontawesome-free-solid';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button, ButtonGroup, Menu, MenuItem, Popover, Position } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { when } from 'mobx';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../constants';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { classNames } from '../../utils/classnames';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../shared-constants';
|
||||
import { getOctokit } from '../../utils/octokit';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
@@ -15,10 +13,6 @@ export interface PublishButtonProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface PublishButtonState {
|
||||
isPublishing: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "publish" button takes care of logging you in.
|
||||
*
|
||||
@@ -27,13 +21,13 @@ export interface PublishButtonState {
|
||||
* @extends {React.Component<PublishButtonProps, PublishButtonState>}
|
||||
*/
|
||||
@observer
|
||||
export class PublishButton extends React.Component<PublishButtonProps, PublishButtonState> {
|
||||
export class PublishButton extends React.Component<PublishButtonProps> {
|
||||
constructor(props: PublishButtonProps) {
|
||||
super(props);
|
||||
|
||||
this.state = { isPublishing: false };
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.publishFiddle = this.publishFiddle.bind(this);
|
||||
this.setPrivate = this.setPrivate.bind(this);
|
||||
this.setPublic = this.setPublic.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
@@ -71,21 +65,17 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
* and update all related properties in the app state.
|
||||
*/
|
||||
public async publishFiddle(): Promise<void> {
|
||||
this.setState({ isPublishing: true });
|
||||
|
||||
const octo = await getOctokit();
|
||||
octo.authenticate({
|
||||
type: 'token',
|
||||
token: this.props.appState.gitHubToken!
|
||||
});
|
||||
const { appState } = this.props;
|
||||
appState.isPublishing = true;
|
||||
|
||||
const octo = await getOctokit(this.props.appState);
|
||||
const { gitHubPublishAsPublic } = this.props.appState;
|
||||
const options = { includeDependencies: true, includeElectron: true };
|
||||
const values = await window.ElectronFiddle.app.getValues(options);
|
||||
|
||||
|
||||
try {
|
||||
const gist = await octo.gists.create({
|
||||
public: true,
|
||||
public: !!gitHubPublishAsPublic,
|
||||
description: 'Electron Fiddle Gist',
|
||||
files: {
|
||||
[INDEX_HTML_NAME]: {
|
||||
@@ -100,7 +90,7 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
},
|
||||
} as any); // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
|
||||
|
||||
this.props.appState.gistId = gist.data.id;
|
||||
appState.gistId = gist.data.id;
|
||||
|
||||
console.log(`Publish Button: Publishing done`, { gist });
|
||||
} catch (error) {
|
||||
@@ -114,21 +104,72 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
|
||||
}
|
||||
|
||||
this.setState({ isPublishing: false });
|
||||
appState.isPublishing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish fiddles as private.
|
||||
*
|
||||
* @memberof PublishButton
|
||||
*/
|
||||
public setPrivate() {
|
||||
this.setPrivacy(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish fiddles as public.
|
||||
*
|
||||
* @memberof PublishButton
|
||||
*/
|
||||
public setPublic() {
|
||||
this.setPrivacy(true);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isPublishing } = this.state;
|
||||
const className = classNames('button', 'button-publish', isPublishing);
|
||||
const icon = isPublishing ? faSpinner : faUpload;
|
||||
const text = isPublishing ? 'Publishing...' : 'Publish';
|
||||
const { gitHubPublishAsPublic } = this.props.appState;
|
||||
const { isPublishing } = this.props.appState;
|
||||
|
||||
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
|
||||
const privacyMenu = (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text='Private'
|
||||
icon='lock'
|
||||
active={!gitHubPublishAsPublic}
|
||||
onClick={this.setPrivate}
|
||||
/>
|
||||
<MenuItem
|
||||
text='Public'
|
||||
icon='unlock'
|
||||
active={gitHubPublishAsPublic}
|
||||
onClick={this.setPublic}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<button className={className} onClick={this.handleClick} disabled={isPublishing}>
|
||||
<FontAwesomeIcon icon={icon} spin={isPublishing} />
|
||||
<span style={{ marginLeft: 8 }} />
|
||||
{text}
|
||||
</button>
|
||||
<fieldset disabled={isPublishing}>
|
||||
<ButtonGroup className='button-publish'>
|
||||
<Popover
|
||||
content={privacyMenu}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
icon={privacyIcon}
|
||||
/>
|
||||
</Popover>
|
||||
<Button
|
||||
onClick={this.handleClick}
|
||||
loading={isPublishing}
|
||||
icon='upload'
|
||||
text={isPublishing ? 'Publishing...' : 'Publish'}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
private setPrivacy(publishAsPublic: boolean) {
|
||||
this.props.appState.gitHubPublishAsPublic = publishAsPublic;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Button, IButtonProps, Spinner } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ElectronVersionState } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface RunnerState {
|
||||
}
|
||||
|
||||
export interface RunnerProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The runner component is responsible for actually launching the fiddle
|
||||
* with Electron. It also renders the button that does so.
|
||||
*
|
||||
* @class Runner
|
||||
* @extends {React.Component<RunnerProps, RunnerState>}
|
||||
*/
|
||||
@observer
|
||||
export class Runner extends React.Component<RunnerProps, RunnerState> {
|
||||
public render() {
|
||||
const { isRunning, currentElectronVersion } = this.props.appState;
|
||||
|
||||
const state = currentElectronVersion && currentElectronVersion.state;
|
||||
const props: IButtonProps = { className: 'button-run' };
|
||||
|
||||
if (state === ElectronVersionState.downloading) {
|
||||
props.text = 'Downloading';
|
||||
props.disabled = true;
|
||||
props.icon = <Spinner size={16} />;
|
||||
} else if (state === ElectronVersionState.ready) {
|
||||
if (isRunning) {
|
||||
props.active = true;
|
||||
props.text = 'Stop';
|
||||
props.onClick = window.ElectronFiddle.app.runner.stop;
|
||||
props.icon = 'stop';
|
||||
} else {
|
||||
props.text = 'Run';
|
||||
props.onClick = window.ElectronFiddle.app.runner.run;
|
||||
props.icon = 'play';
|
||||
}
|
||||
} else {
|
||||
props.text = 'Checking status';
|
||||
props.disabled = true;
|
||||
props.icon = <Spinner size={16} />;
|
||||
}
|
||||
|
||||
return <Button {...props} type={undefined} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { Button, MenuItem } from '@blueprintjs/core';
|
||||
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
|
||||
import { highlightText } from '../../utils/highlight-text';
|
||||
import { sortedElectronMap } from '../../utils/sorted-electron-map';
|
||||
import { AppState } from '../state';
|
||||
import { getReleaseChannel } from '../versions';
|
||||
|
||||
const ElectronVersionSelect = Select.ofType<ElectronVersion>();
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> label for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {ElectronVersion} { source, state }
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getItemLabel({ source, state, name }: ElectronVersion): string {
|
||||
let label = '';
|
||||
|
||||
if (source === ElectronVersionSource.local) {
|
||||
label = name || 'Local';
|
||||
} else {
|
||||
if (state === ElectronVersionState.unknown) {
|
||||
label = `Not downloaded`;
|
||||
} else if (state === ElectronVersionState.ready) {
|
||||
label = `Downloaded`;
|
||||
} else if (state === ElectronVersionState.downloading) {
|
||||
label = `Downloading`;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> icon for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {ElectronVersion} { state }
|
||||
* @returns
|
||||
*/
|
||||
export function getItemIcon({ state }: ElectronVersion) {
|
||||
return state === 'ready'
|
||||
? 'saved'
|
||||
: state === 'downloading' ? 'cloud-download' : 'cloud';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> predicate for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {string} query
|
||||
* @param {ElectronVersion} { version }
|
||||
* @returns
|
||||
*/
|
||||
export const filterItem: ItemPredicate<ElectronVersion> = (query, { version }) => {
|
||||
return version.toLowerCase().includes(query.toLowerCase());
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> <MenuItem /> for Electron
|
||||
* versions.
|
||||
*
|
||||
* @param {ElectronVersion} item
|
||||
* @param {IItemRendererProps} { handleClick, modifiers, query }
|
||||
* @returns
|
||||
*/
|
||||
export const renderItem: ItemRenderer<ElectronVersion> = (item, { handleClick, modifiers, query }) => {
|
||||
if (!modifiers.matchesPredicate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
text={highlightText(item.version, query)}
|
||||
key={item.version}
|
||||
onClick={handleClick}
|
||||
label={getItemLabel(item)}
|
||||
icon={getItemIcon(item)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface VersionChooserState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface VersionChooserProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dropdown allowing the selection of Electron versions. The actual
|
||||
* download is managed in the state.
|
||||
*
|
||||
* @class VersionChooser
|
||||
* @extends {React.Component<VersionChooserProps, VersionChooserState>}
|
||||
*/
|
||||
@observer
|
||||
export class VersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
|
||||
constructor(props: VersionChooserProps) {
|
||||
super(props);
|
||||
|
||||
this.onItemSelect = this.onItemSelect.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle change, which usually means that we'd like update
|
||||
* the selection version.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLSelectElement>} event
|
||||
*/
|
||||
public onItemSelect({ version }: ElectronVersion) {
|
||||
this.props.appState.setVersion(version);
|
||||
}
|
||||
|
||||
public getItems(): Array<ElectronVersion> {
|
||||
const { versions, versionsToShow, statesToShow } = this.props.appState;
|
||||
|
||||
return sortedElectronMap<ElectronVersion>(versions, (_key, item) => item)
|
||||
.filter((item) => {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we want to show the version
|
||||
if (!versionsToShow.includes(getReleaseChannel(item))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we want to show the state
|
||||
if (!statesToShow.includes(item.state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { currentElectronVersion } = this.props.appState;
|
||||
const { version } = currentElectronVersion;
|
||||
|
||||
return (
|
||||
<ElectronVersionSelect
|
||||
filterable={true}
|
||||
items={this.getItems()}
|
||||
itemRenderer={renderItem}
|
||||
itemPredicate={filterItem}
|
||||
onItemSelect={this.onItemSelect}
|
||||
noResults={<MenuItem disabled={true} text='No results.' />}
|
||||
>
|
||||
<Button
|
||||
className='version-chooser'
|
||||
text={`Electron v${version}`}
|
||||
icon={getItemIcon(currentElectronVersion)}
|
||||
/>
|
||||
</ElectronVersionSelect>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { faTerminal } from '@fortawesome/fontawesome-free-solid';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button, ControlGroup } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { AddressBar } from './address-bar';
|
||||
import { PublishButton } from './publish-button';
|
||||
import { Runner } from './runner';
|
||||
import { VersionChooser } from './version-chooser';
|
||||
import { AddressBar } from './commands-address-bar';
|
||||
import { EditorDropdown } from './commands-editors';
|
||||
import { PublishButton } from './commands-publish-button';
|
||||
import { Runner } from './commands-runner';
|
||||
import { VersionChooser } from './commands-version-chooser';
|
||||
|
||||
export interface CommandsProps {
|
||||
appState: AppState;
|
||||
@@ -32,15 +32,23 @@ export class Commands extends React.Component<CommandsProps, {}> {
|
||||
return (
|
||||
<div className='commands'>
|
||||
<div>
|
||||
<Runner appState={appState} />
|
||||
<VersionChooser appState={appState} />
|
||||
<ControlGroup fill={true} vertical={false}>
|
||||
<VersionChooser appState={appState} />
|
||||
<Runner 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>
|
||||
<AddressBar appState={appState} />
|
||||
<PublishButton appState={appState} />
|
||||
<button className='button' onClick={() => appState.toggleConsole()}>
|
||||
<FontAwesomeIcon icon={faTerminal} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
|
||||
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 { GitHubVersion } from '../../interfaces';
|
||||
import { NpmVersion } from '../../interfaces';
|
||||
import { getElectronNameForPlatform } from '../../utils/electron-name';
|
||||
import { AppState } from '../state';
|
||||
import { Dialog } from './dialog';
|
||||
|
||||
export interface AddVersionDialogProps {
|
||||
appState: AppState;
|
||||
@@ -47,8 +48,8 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public async onChangeFile(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { files } = event.target;
|
||||
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
|
||||
const { files } = event.target as any;
|
||||
const { binaryManager } = this.props.appState;
|
||||
const file = files && files[0] ? files[0] : undefined;
|
||||
|
||||
@@ -85,17 +86,10 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
.slice(1)
|
||||
.join(path.sep);
|
||||
|
||||
const toAdd: GitHubVersion = {
|
||||
url: file.path,
|
||||
assets_url: file.path,
|
||||
body: `Local version, added at ${Date.now()}`,
|
||||
created_at: Date.now().toString(),
|
||||
name,
|
||||
html_url: '',
|
||||
prerelease: true,
|
||||
published_at: Date.now().toString(),
|
||||
tag_name: version,
|
||||
target_commitish: ''
|
||||
const toAdd: NpmVersion = {
|
||||
localPath: file.path,
|
||||
version,
|
||||
name
|
||||
};
|
||||
|
||||
this.props.appState.addLocalVersion(toAdd);
|
||||
@@ -110,71 +104,60 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this component's state
|
||||
*/
|
||||
public reset(): void {
|
||||
this.setState({
|
||||
isValidElectron: false,
|
||||
isValidVersion: false,
|
||||
version: '',
|
||||
file: undefined
|
||||
});
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
const canSubmit = this.state.isValidElectron && this.state.isValidVersion;
|
||||
|
||||
return [
|
||||
(
|
||||
<button
|
||||
className='button'
|
||||
<Button
|
||||
icon='add'
|
||||
key='submit'
|
||||
disabled={!canSubmit}
|
||||
onClick={this.onSubmit}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
text='Add'
|
||||
/>
|
||||
), (
|
||||
<button
|
||||
className='cancel'
|
||||
<Button
|
||||
icon='cross'
|
||||
key='cancel'
|
||||
onClick={this.onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
text='Cancel'
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isAddVersionDialogShowing } = this.props.appState;
|
||||
const dirOptions = { webkitdirectory: 'true' };
|
||||
const inputProps = { webkitdirectory: 'true' };
|
||||
const { file } = this.state;
|
||||
|
||||
const text = file && file.path
|
||||
? file.path
|
||||
: `Select the folder containing ${getElectronNameForPlatform()}...`;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isShowing={isAddVersionDialogShowing}
|
||||
isShowingBackdrop={true}
|
||||
buttons={this.buttons}
|
||||
isOpen={isAddVersionDialogShowing}
|
||||
onClose={this.onClose}
|
||||
isCentered={true}
|
||||
className='add-version-dialog'
|
||||
key='add-version-dialog'
|
||||
title='Add local Electron build'
|
||||
className='dialog-add-version'
|
||||
>
|
||||
<label
|
||||
htmlFor='custom-electron-version'
|
||||
className='force-button'
|
||||
>
|
||||
Select the folder containing {getElectronNameForPlatform()}
|
||||
</label>
|
||||
<input
|
||||
type='file'
|
||||
onChange={this.onChangeFile}
|
||||
id='custom-electron-version'
|
||||
name='custom-electron-version'
|
||||
{...dirOptions}
|
||||
/>
|
||||
{this.renderPath()}
|
||||
<div className='bp3-dialog-body'>
|
||||
<FileInput
|
||||
onInputChange={this.onChangeFile}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -189,16 +172,10 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
: `We did not find a ${getElectronNameForPlatform()} in this folder...`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
readOnly={true}
|
||||
value={file.path}
|
||||
/>
|
||||
<p>
|
||||
{info}
|
||||
</p>
|
||||
<Callout>
|
||||
{info}
|
||||
{this.renderVersionInput()}
|
||||
</>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -213,8 +190,8 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
Please specify a version, used for typings and the name.
|
||||
Must be <code>semver</code> compliant.
|
||||
</p>
|
||||
<input
|
||||
className={isValidVersion ? '' : 'hasError'}
|
||||
<InputGroup
|
||||
intent={isValidVersion ? undefined : Intent.DANGER}
|
||||
value={version}
|
||||
onChange={this.onChangeVersion}
|
||||
placeholder='4.0.0'
|
||||
@@ -223,4 +200,15 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this component's state
|
||||
*/
|
||||
private reset(): void {
|
||||
this.setState({
|
||||
isValidElectron: false,
|
||||
isValidVersion: false,
|
||||
version: '',
|
||||
file: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ConfirmDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface ConfirmDialogState {
|
||||
}
|
||||
|
||||
/**
|
||||
* The token dialog prompts the user to either continue or cancel the operation.
|
||||
*
|
||||
* @export
|
||||
* @class ConfirmDialog
|
||||
* @extends {React.Component<ConfirmDialogProps, ConfirmDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class ConfirmDialog extends React.Component<ConfirmDialogProps, ConfirmDialogState> {
|
||||
constructor(props: ConfirmDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
public onClose(result: boolean) {
|
||||
this.props.appState.confirmationPromptLastResult = result;
|
||||
this.props.appState.toogleConfirmationPromptDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isConfirmationPromptShowing, confirmationDialogTexts } = this.props.appState;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
isOpen={isConfirmationPromptShowing}
|
||||
onClose={this.onClose}
|
||||
icon='help'
|
||||
confirmButtonText={confirmationDialogTexts.ok}
|
||||
cancelButtonText={confirmationDialogTexts.cancel}
|
||||
intent={Intent.PRIMARY}
|
||||
>
|
||||
<p>{confirmationDialogTexts.label}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import { faKey, faSpinner } from '@fortawesome/fontawesome-free-solid';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
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';
|
||||
import { Dialog } from './dialog';
|
||||
|
||||
export interface TokenDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface TokenDialogState {
|
||||
tokenInput?: string;
|
||||
tokenInput: string;
|
||||
verifying: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
@@ -32,16 +31,15 @@ const GENERATE_TOKEN_URL = `https://github.com/settings/tokens/new?scopes=${TOKE
|
||||
*/
|
||||
@observer
|
||||
export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogState> {
|
||||
private tokenInputRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
constructor(props: TokenDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
verifying: false,
|
||||
error: false,
|
||||
tokenInput: ''
|
||||
};
|
||||
this.tokenInputRef = React.createRef();
|
||||
|
||||
this.onSubmitToken = this.onSubmitToken.bind(this);
|
||||
this.openGenerateTokenExternal = this.openGenerateTokenExternal.bind(this);
|
||||
this.onTokenInputFocused = this.onTokenInputFocused.bind(this);
|
||||
@@ -57,31 +55,24 @@ export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogSt
|
||||
*/
|
||||
public async onSubmitToken(): Promise<void> {
|
||||
if (!this.state.tokenInput) return;
|
||||
this.setState({
|
||||
verifying: true,
|
||||
});
|
||||
this.setState({ verifying: true });
|
||||
this.props.appState.gitHubToken = this.state.tokenInput;
|
||||
|
||||
const octo = await getOctokit();
|
||||
octo.authenticate({
|
||||
type: 'token',
|
||||
token: this.state.tokenInput || '',
|
||||
});
|
||||
const octo = await getOctokit(this.props.appState);
|
||||
|
||||
try {
|
||||
const { data } = await octo.users.get({});
|
||||
const { data } = await octo.users.getAuthenticated();
|
||||
this.props.appState.gitHubAvatarUrl = data.avatar_url;
|
||||
this.props.appState.gitHubLogin = data.login;
|
||||
this.props.appState.gitHubName = data.name;
|
||||
this.setState({ verifying: false, error: false });
|
||||
} catch (error) {
|
||||
console.warn(`Authenticating against GitHub failed`, error);
|
||||
this.setState({ verifying: false, error: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.appState.gitHubToken = this.state.tokenInput;
|
||||
this.props.appState.isTokenDialogShowing = false;
|
||||
|
||||
this.setState({ verifying: false, error: false });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,64 +129,68 @@ export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogSt
|
||||
this.setState({ tokenInput: event.target.value });
|
||||
}
|
||||
|
||||
/**
|
||||
* A spinner element!
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
get spinner() {
|
||||
if (!this.state.verifying) return null;
|
||||
|
||||
return (
|
||||
<div className='tokenSpinner'>
|
||||
<FontAwesomeIcon icon={faSpinner} spin={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
const canSubmit = !!this.state.tokenInput;
|
||||
|
||||
return [
|
||||
(
|
||||
<button
|
||||
className='button'
|
||||
<Button
|
||||
key='done'
|
||||
disabled={!canSubmit}
|
||||
onClick={this.onSubmitToken}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
loading={this.state.verifying}
|
||||
text='Done'
|
||||
icon='log-in'
|
||||
/>
|
||||
), (
|
||||
<button className='cancel' onClick={this.onClose}>Cancel</button>
|
||||
<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
|
||||
isShowing={isTokenDialogShowing}
|
||||
isShowingBackdrop={true}
|
||||
buttons={this.buttons}
|
||||
isOpen={isTokenDialogShowing}
|
||||
onClose={this.onClose}
|
||||
isCentered={true}
|
||||
className='tokenDialog'
|
||||
key='tokenDialog'
|
||||
title='GitHub Token'
|
||||
>
|
||||
{this.spinner}
|
||||
<span className='generateTokenText'>
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
Generate a <a onClick={this.openGenerateTokenExternal}>GitHub Personal Access Token</a> and paste it here:
|
||||
</span>
|
||||
<input
|
||||
ref={this.tokenInputRef}
|
||||
value={this.state.tokenInput || ''}
|
||||
onFocus={this.onTokenInputFocused}
|
||||
onChange={this.handleChange}
|
||||
className={this.state.error ? 'hasError' : ''}
|
||||
/>
|
||||
<div className='bp3-dialog-body'>
|
||||
<p>
|
||||
Generate a <a onClick={this.openGenerateTokenExternal}>GitHub Personal Access Token</a> and paste it here:
|
||||
</p>
|
||||
|
||||
{this.state.error ? this.invalidWarning : null}
|
||||
|
||||
<InputGroup
|
||||
value={this.state.tokenInput || ''}
|
||||
onFocus={this.onTokenInputFocused}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='bp3-dialog-footer'>
|
||||
<div className='bp3-dialog-footer-actions'>
|
||||
{this.buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface WarningDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface WarningDialogState {
|
||||
}
|
||||
|
||||
/**
|
||||
* The token dialog prompts the user to either continue or cancel the operation.
|
||||
*
|
||||
* @export
|
||||
* @class WarningDialog
|
||||
* @extends {React.Component<WarningDialogProps, WarningDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class WarningDialog extends React.Component<WarningDialogProps, WarningDialogState> {
|
||||
constructor(props: WarningDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
public onClose(result: boolean) {
|
||||
this.props.appState.warningDialogLastResult = result;
|
||||
this.props.appState.toogleWarningDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isWarningDialogShowing, warningDialogTexts } = this.props.appState;
|
||||
return (
|
||||
<Alert
|
||||
isOpen={isWarningDialogShowing}
|
||||
onClose={this.onClose}
|
||||
icon='warning-sign'
|
||||
confirmButtonText={warningDialogTexts.ok}
|
||||
cancelButtonText={warningDialogTexts.cancel}
|
||||
intent={Intent.DANGER}
|
||||
>
|
||||
<p>{warningDialogTexts.label}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ArrowPosition } from '../../interfaces';
|
||||
import { classNames } from '../../utils/classnames';
|
||||
import { sortButtons } from '../../utils/sort-buttons';
|
||||
|
||||
export interface DialogProps {
|
||||
buttons?: Array<JSX.Element> | null;
|
||||
style?: React.CSSProperties;
|
||||
isCentered?: boolean;
|
||||
className?: string;
|
||||
isShowing?: boolean;
|
||||
isShowingBackdrop?: boolean;
|
||||
arrow?: ArrowPosition;
|
||||
onClose?: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic dialog class for Fiddle.
|
||||
*
|
||||
* @class Dialog
|
||||
* @extends {React.Component<DialogProps, {}>}
|
||||
*/
|
||||
export class Dialog extends React.Component<DialogProps, {}> {
|
||||
public static defaultProps: Partial<DialogProps> = {
|
||||
isCentered: false,
|
||||
isShowing: true,
|
||||
isShowingBackdrop: false
|
||||
};
|
||||
|
||||
constructor(props: DialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onConfirm = this.onConfirm.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog
|
||||
*/
|
||||
public onClose() {
|
||||
if (this.props.onClose) this.props.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the dialog
|
||||
*/
|
||||
public onConfirm() {
|
||||
if (this.props.onConfirm) this.props.onConfirm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the dialog's buttons, using a default "Ok"
|
||||
* if none were passed.
|
||||
*
|
||||
* @returns {Array<JSX.Element> | JSX.Element}
|
||||
*/
|
||||
public renderButtons(): Array<JSX.Element | null> | JSX.Element {
|
||||
const { buttons, onClose } = this.props;
|
||||
const closeButton = onClose
|
||||
? <button key='cancel' className='btn-close' onClick={this.onClose}>Cancel</button>
|
||||
: null;
|
||||
const okButton = (
|
||||
<button key='ok' className='btn-ok' onClick={this.onConfirm}>Ok</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='dialog-buttons'>
|
||||
{sortButtons(buttons! || [ closeButton, okButton ])}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally renders a backdrop that closes the
|
||||
* dialog if clicked.
|
||||
*
|
||||
* @returns {JSX.Element | null}
|
||||
*/
|
||||
public renderBackdrop(): JSX.Element | null {
|
||||
const { isShowingBackdrop } = this.props;
|
||||
|
||||
if (!isShowingBackdrop) return null;
|
||||
|
||||
return (
|
||||
<div key='drop' className='dialogDrop' onClick={this.onClose} />
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isShowing, isCentered, style, children, className, arrow } = this.props;
|
||||
const arrowClass = arrow ? `arrow arrow-${arrow}` : '';
|
||||
const parsedClassName = classNames(
|
||||
'dialog',
|
||||
arrowClass,
|
||||
{ centered: isCentered },
|
||||
className
|
||||
);
|
||||
|
||||
return isShowing ? (
|
||||
<>
|
||||
{this.renderBackdrop()}
|
||||
<div style={style} className={parsedClassName}>
|
||||
{children}
|
||||
{this.renderButtons()}
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { AddVersionDialog } from './add-version-dialog';
|
||||
import { AddVersionDialog } from './dialog-add-version';
|
||||
import { ConfirmDialog } from './dialog-confirm';
|
||||
import { TokenDialog } from './dialog-token';
|
||||
import { WarningDialog } from './dialog-warning';
|
||||
import { Settings } from './settings';
|
||||
import { TokenDialog } from './token-dialog';
|
||||
|
||||
export interface DialogsProps {
|
||||
appState: AppState;
|
||||
@@ -30,12 +32,16 @@ export class Dialogs extends React.Component<DialogsProps, {}> {
|
||||
const maybeAddLocalVersion = isAddVersionDialogShowing
|
||||
? <AddVersionDialog key='add-version-dialog' appState={appState} />
|
||||
: null;
|
||||
const eitherWarningOrPrompt = appState.isWarningDialogShowing
|
||||
? <WarningDialog appState={appState} />
|
||||
: <ConfirmDialog appState={appState}/>;
|
||||
|
||||
return (
|
||||
<div key='dialogs' className='dialogs'>
|
||||
{maybeToken}
|
||||
{maybeSettings}
|
||||
{maybeAddLocalVersion}
|
||||
{eitherWarningOrPrompt}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ContentNames, getContent } from '../content';
|
||||
import { EditorId } from '../../interfaces';
|
||||
import { getContent } from '../content';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface EditorProps {
|
||||
appState: AppState;
|
||||
monaco: typeof MonacoType;
|
||||
monoacoOptions: MonacoType.editor.IEditorOptions;
|
||||
id: string;
|
||||
id: EditorId;
|
||||
options?: Partial<MonacoType.editor.IEditorConstructionOptions>;
|
||||
editorDidMount?: (editor: MonacoType.editor.IStandaloneCodeEditor) => void;
|
||||
onChange?: (value: string, event: MonacoType.editor.IModelContentChangedEvent) => void;
|
||||
@@ -48,11 +49,16 @@ export class Editor extends React.Component<EditorProps> {
|
||||
*
|
||||
* @param {MonacoType.editor.IStandaloneCodeEditor} editor
|
||||
*/
|
||||
public editorDidMount(editor: MonacoType.editor.IStandaloneCodeEditor) {
|
||||
public async editorDidMount(editor: MonacoType.editor.IStandaloneCodeEditor) {
|
||||
const { editorDidMount } = this.props;
|
||||
|
||||
// Set the content on the editor
|
||||
await this.setContent();
|
||||
|
||||
// Set the editor as an available object
|
||||
window.ElectronFiddle.editors[this.props.id] = editor;
|
||||
|
||||
// And notify others
|
||||
if (editorDidMount) {
|
||||
editorDidMount(editor);
|
||||
}
|
||||
@@ -62,8 +68,7 @@ export class Editor extends React.Component<EditorProps> {
|
||||
* Initialize Monaco.
|
||||
*/
|
||||
public async initMonaco() {
|
||||
const { monaco, id, appState, monoacoOptions } = this.props;
|
||||
const { version } = appState;
|
||||
const { monaco, monoacoOptions } = this.props;
|
||||
const ref = this.containerRef.current;
|
||||
|
||||
if (ref) {
|
||||
@@ -71,10 +76,11 @@ export class Editor extends React.Component<EditorProps> {
|
||||
language: this.language,
|
||||
theme: 'main',
|
||||
contextmenu: false,
|
||||
value: await getContent(id as ContentNames, version),
|
||||
model: null,
|
||||
...monoacoOptions
|
||||
});
|
||||
this.editorDidMount(this.editor);
|
||||
|
||||
await this.editorDidMount(this.editor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +89,7 @@ export class Editor extends React.Component<EditorProps> {
|
||||
*/
|
||||
public destroyMonaco() {
|
||||
if (typeof this.editor !== 'undefined') {
|
||||
console.log('Editor: Disposing');
|
||||
this.editor.dispose();
|
||||
}
|
||||
}
|
||||
@@ -90,4 +97,32 @@ export class Editor extends React.Component<EditorProps> {
|
||||
public render() {
|
||||
return <div className='editorContainer' ref={this.containerRef} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content on the editor, including the model and the view state.
|
||||
*
|
||||
* @private
|
||||
* @memberof Editor
|
||||
*/
|
||||
private async setContent() {
|
||||
const { appState, id, monaco } = this.props;
|
||||
const { version } = appState;
|
||||
|
||||
const backup = appState.getAndRemoveEditorValueBackup(id);
|
||||
|
||||
if (backup && backup.model) {
|
||||
console.log(`Editor: Backup found, restoring state`);
|
||||
|
||||
if (backup.viewState) {
|
||||
this.editor.restoreViewState(backup.viewState);
|
||||
}
|
||||
|
||||
this.editor.setModel(backup.model);
|
||||
} else {
|
||||
const value = await getContent(id, version);
|
||||
const model = monaco.editor.createModel(value, this.language);
|
||||
|
||||
this.editor.setModel(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, NonIdealState } from '@blueprintjs/core';
|
||||
|
||||
import { EditorId } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export function renderNonIdealState(appState: AppState) {
|
||||
const allEditors = [ EditorId.html, EditorId.main, EditorId.renderer ];
|
||||
const resolveButton = (
|
||||
<Button
|
||||
text='Open all editors'
|
||||
onClick={() => appState.setVisibleMosaics(allEditors)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NonIdealState
|
||||
action={resolveButton}
|
||||
icon='applications'
|
||||
description='You have closed all editors. You can open them again with the button below or the "Editors" button above!'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { MosaicContext, MosaicRootActions, MosaicWindowContext } from 'react-mosaic-component';
|
||||
|
||||
import { DocsDemoPage, MosaicId } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ToolbarButtonProps {
|
||||
appState: AppState;
|
||||
id: MosaicId;
|
||||
}
|
||||
|
||||
export abstract class ToolbarButton extends React.PureComponent<ToolbarButtonProps> {
|
||||
public static contextType = MosaicWindowContext;
|
||||
public context: MosaicWindowContext;
|
||||
|
||||
constructor(props: ToolbarButtonProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<MosaicContext.Consumer>
|
||||
{({ mosaicActions }) => this.createButton(mosaicActions)}
|
||||
</MosaicContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a button that performs the actual action
|
||||
*/
|
||||
public abstract createButton(_mosaicActions: MosaicRootActions<any>): React.ReactNode;
|
||||
}
|
||||
|
||||
export class MaximizeButton extends ToolbarButton {
|
||||
/**
|
||||
* Create a button that can expand this panel
|
||||
*/
|
||||
public createButton(mosaicActions: MosaicRootActions<any>) {
|
||||
const onClick = () => {
|
||||
mosaicActions.expand(this.context.mosaicWindowActions.getPath());
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon='maximize'
|
||||
className='bp3-small'
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveButton extends ToolbarButton {
|
||||
/**
|
||||
* Create a button that can remove this panel
|
||||
*/
|
||||
public createButton(_mosaicActions: MosaicRootActions<any>) {
|
||||
const onClick = () => this.props.appState.hideAndBackupMosaic(this.props.id);
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon='cross'
|
||||
className='bp3-small'
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class DocsDemoGoHomeButton extends ToolbarButton {
|
||||
/**
|
||||
* Create a button that can remove this panel
|
||||
*/
|
||||
public createButton(_mosaicActions: MosaicRootActions<any>) {
|
||||
const onClick = () => this.props.appState.currentDocsDemoPage = DocsDemoPage.DEFAULT;
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon='home'
|
||||
className='bp3-small'
|
||||
onClick={onClick}
|
||||
text='Overview'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
import { reaction } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as React from 'react';
|
||||
import { Mosaic, MosaicWindow } from 'react-mosaic-component';
|
||||
import { Mosaic, MosaicBranch, MosaicNode, MosaicWindow, MosaicWindowProps } from 'react-mosaic-component';
|
||||
|
||||
import { EditorId } from '../../interfaces';
|
||||
import { EditorId, MosaicId, PanelId } from '../../interfaces';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { updateEditorLayout } from '../../utils/editor-layout';
|
||||
import { getFocusedEditor } from '../../utils/focused-editor';
|
||||
import { getAtPath, setAtPath } from '../../utils/js-path';
|
||||
import { toggleMonaco } from '../../utils/toggle-monaco';
|
||||
import { ContentNames, getContent } from '../content';
|
||||
import { isEditorId } from '../../utils/type-checks';
|
||||
import { getContent } from '../content';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
import { activateTheme } from '../themes';
|
||||
import { Editor } from './editor';
|
||||
|
||||
const options: any = {
|
||||
direction: 'row',
|
||||
first: 'main',
|
||||
second: {
|
||||
direction: 'column',
|
||||
first: 'renderer',
|
||||
second: 'html',
|
||||
},
|
||||
};
|
||||
import { renderNonIdealState } from './editors-non-ideal-state';
|
||||
import { DocsDemoGoHomeButton, MaximizeButton, RemoveButton } from './editors-toolbar-button';
|
||||
import { ShowMe } from './show-me';
|
||||
|
||||
const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
|
||||
minimap: {
|
||||
@@ -31,13 +27,11 @@ const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
|
||||
wordWrap: 'on'
|
||||
};
|
||||
|
||||
const ViewIdMosaic = Mosaic.ofType<EditorId>();
|
||||
const ViewIdMosaicWindow = MosaicWindow.ofType<EditorId>();
|
||||
|
||||
const TITLE_MAP: Record<EditorId, string> = {
|
||||
main: 'Main Process',
|
||||
renderer: 'Renderer Process',
|
||||
html: 'HTML'
|
||||
export const TITLE_MAP: Record<MosaicId, string> = {
|
||||
main: 'Main Process (main.js)',
|
||||
renderer: 'Renderer Process (renderer.js)',
|
||||
html: 'HTML (index.html)',
|
||||
docsDemo: 'Docs & Demos'
|
||||
};
|
||||
|
||||
export interface EditorsProps {
|
||||
@@ -50,13 +44,33 @@ export interface EditorsState {
|
||||
monacoOptions: MonacoType.editor.IEditorOptions;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
// A reaction: Each time mosaicArrangement is changed, we'll update
|
||||
// the editor layout. That method is itself debounced.
|
||||
public disposeLayoutAutorun = reaction(
|
||||
() => this.props.appState.mosaicArrangement,
|
||||
() => updateEditorLayout()
|
||||
);
|
||||
|
||||
constructor(props: EditorsProps) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.renderEditor = this.renderEditor.bind(this);
|
||||
this.renderTile = this.renderTile.bind(this);
|
||||
this.renderGenericPanel = this.renderGenericPanel.bind(this);
|
||||
|
||||
this.state = { monacoOptions: defaultMonacoOptions };
|
||||
|
||||
this.loadMonaco();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed right after the component mounts. We'll setup the IPC listeners here.
|
||||
*
|
||||
* @memberof Editors
|
||||
*/
|
||||
public componentDidMount() {
|
||||
ipcRendererManager.on(IpcEvents.MONACO_EXECUTE_COMMAND, (_event, cmd: string) => {
|
||||
this.executeCommand(cmd);
|
||||
@@ -65,24 +79,30 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
ipcRendererManager.on(IpcEvents.FS_NEW_FIDDLE, async (_event) => {
|
||||
const { version } = this.props.appState;
|
||||
|
||||
window.ElectronFiddle.app.setValues({
|
||||
html: await getContent(ContentNames.HTML, version),
|
||||
renderer: await getContent(ContentNames.RENDERER, version),
|
||||
main: await getContent(ContentNames.MAIN, version)
|
||||
this.props.appState.setWarningDialogTexts({
|
||||
label: 'Your current fiddle is unsaved. Do you want to discard it?'
|
||||
});
|
||||
|
||||
window.ElectronFiddle.app.fileManager.setFiddle({
|
||||
values: {
|
||||
html: await getContent(EditorId.html, version),
|
||||
renderer: await getContent(EditorId.renderer, version),
|
||||
main: await getContent(EditorId.main, version),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ipcRendererManager.on(IpcEvents.TOGGLE_SOFT_WRAP, async (_event) => {
|
||||
this.toggleEditorOption('wordWrap');
|
||||
});
|
||||
|
||||
ipcRendererManager.on(IpcEvents.TOGGLE_MINI_MAP, async (_event) => {
|
||||
this.toggleEditorOption('minimap.enabled');
|
||||
ipcRendererManager.on(IpcEvents.MONACO_TOGGLE_OPTION, (_event, cmd: string) => {
|
||||
this.toggleEditorOption(cmd);
|
||||
});
|
||||
|
||||
this.setState({ isMounted: true });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.disposeLayoutAutorun();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to execute a given commandId on the currently focused editor
|
||||
*
|
||||
@@ -103,9 +123,9 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
}
|
||||
}
|
||||
|
||||
public toggleEditorOption(path: string) {
|
||||
if (!window || !window.ElectronFiddle || !window.ElectronFiddle.editors) {
|
||||
return;
|
||||
public toggleEditorOption(path: string): boolean {
|
||||
if (!window.ElectronFiddle.editors) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -116,21 +136,116 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
setAtPath(path, newOptions, toggleMonaco(currentSetting));
|
||||
|
||||
Object.keys(window.ElectronFiddle.editors)
|
||||
.forEach((key) => {
|
||||
const editor: MonacoType.editor.IStandaloneCodeEditor | null
|
||||
= window.ElectronFiddle.editors[key];
|
||||
.forEach((key) => {
|
||||
const editor: MonacoType.editor.IStandaloneCodeEditor | null
|
||||
= window.ElectronFiddle.editors[key];
|
||||
|
||||
if (editor) {
|
||||
editor.updateOptions(newOptions);
|
||||
}
|
||||
});
|
||||
if (editor) {
|
||||
editor.updateOptions(newOptions);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ monacoOptions: newOptions });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Editors: Could not toggle property ${path}`, error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the little tool bar on top of each panel
|
||||
*
|
||||
* @param {MosaicWindowProps<MosaicId>} { title }
|
||||
* @param {MosaicId} id
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
public renderToolbar(
|
||||
{ title }: MosaicWindowProps<MosaicId>, id: MosaicId
|
||||
): JSX.Element {
|
||||
const docsDemoGoHomeMaybe = id === PanelId.docsDemo
|
||||
? <DocsDemoGoHomeButton id={id} appState={this.props.appState} />
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Left */}
|
||||
<div>
|
||||
<h5>
|
||||
{title}
|
||||
</h5>
|
||||
</div>
|
||||
{/* Middle */}
|
||||
<div />
|
||||
{/* Right */}
|
||||
<div className='mosaic-controls'>
|
||||
{docsDemoGoHomeMaybe}
|
||||
<MaximizeButton id={id} appState={this.props.appState} />
|
||||
<RemoveButton id={id} appState={this.props.appState} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Mosaic tile
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {string} path
|
||||
* @returns {JSX.Element | null}
|
||||
*/
|
||||
public renderTile(id: MosaicId, path: Array<MosaicBranch>): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const content = isEditorId(id)
|
||||
? this.renderEditor(id)
|
||||
: this.renderGenericPanel(id, appState);
|
||||
|
||||
return (
|
||||
<MosaicWindow<EditorId>
|
||||
className={id}
|
||||
path={path}
|
||||
title={TITLE_MAP[id]}
|
||||
renderToolbar={(props: MosaicWindowProps<MosaicId>) => this.renderToolbar(props, id)}
|
||||
>
|
||||
{content}
|
||||
</MosaicWindow>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a generic panel – so not an editor, but something else
|
||||
*
|
||||
* @param {PanelId} _id
|
||||
* @param {AppState} _appState
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
public renderGenericPanel(_id: PanelId, appState: AppState): JSX.Element {
|
||||
return <ShowMe appState={appState} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an editor
|
||||
*
|
||||
* @param {EditorId} id
|
||||
* @returns {(JSX.Element | null)}
|
||||
* @memberof Editors
|
||||
*/
|
||||
public renderEditor(id: EditorId): JSX.Element | null {
|
||||
const { appState } = this.props;
|
||||
const { monaco } = this.state;
|
||||
|
||||
return (
|
||||
<Editor
|
||||
id={id}
|
||||
monaco={monaco!}
|
||||
appState={appState}
|
||||
monoacoOptions={defaultMonacoOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { monaco } = this.state;
|
||||
@@ -138,27 +253,30 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
if (!monaco) return null;
|
||||
|
||||
return (
|
||||
<ViewIdMosaic
|
||||
onChange={updateEditorLayout}
|
||||
// tslint:disable-next-line:jsx-no-multiline-js
|
||||
renderTile={(id, path) => (
|
||||
<ViewIdMosaicWindow className={id} path={path} title={TITLE_MAP[id]} toolbarControls={<div />}>
|
||||
<Editor id={id} monaco={monaco} appState={appState} monoacoOptions={defaultMonacoOptions} />
|
||||
</ViewIdMosaicWindow>
|
||||
)}
|
||||
initialValue={options}
|
||||
<Mosaic<EditorId | PanelId>
|
||||
onChange={this.onChange}
|
||||
value={appState.mosaicArrangement}
|
||||
zeroStateView={renderNonIdealState(appState)}
|
||||
renderTile={this.renderTile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in the visible nodes
|
||||
*
|
||||
* @param {(MosaicNode<EditorId> | null)} currentNode
|
||||
*/
|
||||
public onChange(currentNode: MosaicNode<EditorId> | null) {
|
||||
this.props.appState.mosaicArrangement = currentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads monaco. If it's already loaded, it'll just set it on the current state.
|
||||
* We're doing things a bit roundabout to ensure that we're not overloading the
|
||||
* mobx state with a gigantic Monaco tree.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private async loadMonaco(): Promise<void> {
|
||||
public async loadMonaco(): Promise<void> {
|
||||
const { app } = window.ElectronFiddle;
|
||||
const loader = require('monaco-loader');
|
||||
const monaco = app.monaco || await loader();
|
||||
@@ -168,7 +286,10 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
}
|
||||
|
||||
if (!this.state || !this.state.isMounted) {
|
||||
this.state = { monaco, monacoOptions: defaultMonacoOptions };
|
||||
this.state = {
|
||||
monaco,
|
||||
monacoOptions: defaultMonacoOptions
|
||||
};
|
||||
} else {
|
||||
this.setState({ monaco });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { AppState } from '../state';
|
||||
import { ChromeMac } from './chrome-mac';
|
||||
import { Commands } from './commands';
|
||||
import { Output } from './output';
|
||||
import { WelcomeTour } from './tour-welcome';
|
||||
|
||||
export interface HeaderProps {
|
||||
@@ -23,7 +22,6 @@ export class Header extends React.Component<HeaderProps, {}> {
|
||||
<ChromeMac appState={this.props.appState} />
|
||||
<header id='header'>
|
||||
<Commands key='commands' appState={this.props.appState} />
|
||||
<Output key='output' appState={this.props.appState} />
|
||||
</header>
|
||||
<WelcomeTour appState={this.props.appState} />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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) => {
|
||||
this.setState({mosaicArrangement: currentNode});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { autorun } from 'mobx';
|
||||
import { MosaicContext } from 'react-mosaic-component';
|
||||
import { OutputEntry } from '../../interfaces';
|
||||
import { AppState } from '../state';
|
||||
import { WrapperMosaicId } from './output-editors-wrapper';
|
||||
|
||||
export interface CommandsProps {
|
||||
appState: AppState;
|
||||
// Used to keep testing conform
|
||||
renderTimestamp?: (ts: number) => string;
|
||||
}
|
||||
|
||||
@@ -18,6 +22,8 @@ export interface CommandsProps {
|
||||
*/
|
||||
@observer
|
||||
export class Output extends React.Component<CommandsProps, {}> {
|
||||
public static contextType = MosaicContext;
|
||||
public context: MosaicContext<WrapperMosaicId>;
|
||||
private outputRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: CommandsProps) {
|
||||
@@ -27,6 +33,23 @@ export class Output extends React.Component<CommandsProps, {}> {
|
||||
this.renderEntry = this.renderEntry.bind(this);
|
||||
}
|
||||
|
||||
|
||||
public componentDidMount() {
|
||||
autorun(() => {
|
||||
// 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) {
|
||||
const { isConsoleShowing } = this.props.appState;
|
||||
|
||||
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;
|
||||
@@ -71,8 +94,7 @@ export class Output extends React.Component<CommandsProps, {}> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isConsoleShowing, output } = this.props.appState;
|
||||
const className = isConsoleShowing ? 'output showing' : 'output';
|
||||
const { output } = this.props.appState;
|
||||
|
||||
// The last 1000 lines
|
||||
const lines = output
|
||||
@@ -80,7 +102,7 @@ export class Output extends React.Component<CommandsProps, {}> {
|
||||
.map(this.renderEntry);
|
||||
|
||||
return (
|
||||
<div className={className} ref={this.outputRef}>
|
||||
<div className='output' ref={this.outputRef}>
|
||||
{lines}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
import { Callout, Card } from '@blueprintjs/core';
|
||||
import { shell } from 'electron';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface CreditsSettingsProps {
|
||||
appState: AppState;
|
||||
contributors?: Array<any>;
|
||||
}
|
||||
|
||||
export interface CreditsSettingsState {
|
||||
contributors: Array<Contributor>;
|
||||
}
|
||||
|
||||
export interface Contributor {
|
||||
url: string;
|
||||
api: string;
|
||||
login: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
bio: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage Credits-related preferences.
|
||||
*
|
||||
* @class CreditsSettings
|
||||
* @extends {React.Component<CreditsSettingsProps, {}>}
|
||||
* @extends {React.Component<CreditsSettingsProps, CreditsSettingsState>}
|
||||
*/
|
||||
export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
|
||||
private contributors: Array<any> | null = null;
|
||||
export class CreditsSettings extends React.Component<CreditsSettingsProps, CreditsSettingsState> {
|
||||
constructor(props: CreditsSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contributors: []
|
||||
};
|
||||
|
||||
this.getContributors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a list of contributors of Electron Fiddle.
|
||||
@@ -23,15 +47,9 @@ export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
public renderContributors(): Array<JSX.Element> {
|
||||
this.contributors = this.contributors
|
||||
|| this.props.contributors
|
||||
|| require('../../../static/contributors.json');
|
||||
const { contributors } = this.state;
|
||||
|
||||
if (!this.contributors || !Array.isArray(this.contributors)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.contributors.map(({ name, avatar, url, login, location, bio }) => {
|
||||
return contributors.map(({ name, avatar, url, login, location, bio }) => {
|
||||
const maybeLocation = location
|
||||
? <p className='location'>📍 {location}</p>
|
||||
: null;
|
||||
@@ -44,14 +62,14 @@ export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
|
||||
const onClick = () => shell.openExternal(url);
|
||||
|
||||
return (
|
||||
<div key={login} className='contributor' onClick={onClick}>
|
||||
<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>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -60,15 +78,26 @@ export class CreditsSettings extends React.Component<CreditsSettingsProps, {}> {
|
||||
return (
|
||||
<div>
|
||||
<h2>Credits</h2>
|
||||
<p>
|
||||
<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:
|
||||
</p>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import {
|
||||
faClipboardList,
|
||||
faCloudDownloadAlt,
|
||||
faFile,
|
||||
faSpinner,
|
||||
faTrash
|
||||
} from '@fortawesome/fontawesome-free-solid';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Callout,
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
HTMLTable,
|
||||
IButtonProps,
|
||||
Icon,
|
||||
IconName
|
||||
} from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ElectronVersion, ElectronVersionSource } from '../../interfaces';
|
||||
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
|
||||
import { normalizeVersion } from '../../utils/normalize-version';
|
||||
import { sortedElectronMap } from '../../utils/sorted-electron-map';
|
||||
import { AppState } from '../state';
|
||||
@@ -38,7 +41,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
this.handleDownloadAll = this.handleDownloadAll.bind(this);
|
||||
this.handleDeleteAll = this.handleDeleteAll.bind(this);
|
||||
this.handleChannelChange = this.handleChannelChange.bind(this);
|
||||
this.handlePagesChange = this.handlePagesChange.bind(this);
|
||||
this.handleStateChange = this.handleStateChange.bind(this);
|
||||
this.handleDownloadClick = this.handleDownloadClick.bind(this);
|
||||
this.handleAddVersion = this.handleAddVersion.bind(this);
|
||||
|
||||
@@ -53,15 +56,21 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in how many pages should be displayed.
|
||||
* Handles a change in which channels should be displayed.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLSelectElement>} event
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handlePagesChange(
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
public handleStateChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { value } = event.currentTarget;
|
||||
this.props.appState.versionPagesToFetch = parseInt(value, 10) / 30;
|
||||
const { id, checked } = event.currentTarget;
|
||||
const { appState } = this.props;
|
||||
|
||||
if (!checked) {
|
||||
appState.statesToShow = appState.statesToShow.filter((s) => s !== id);
|
||||
} else {
|
||||
appState.statesToShow.push(id as ElectronVersionState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +79,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleChannelChange(
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { id, checked } = event.currentTarget;
|
||||
const { appState } = this.props;
|
||||
@@ -130,14 +139,15 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
return (
|
||||
<div className='settings-electron'>
|
||||
<h2>Electron Settings</h2>
|
||||
<div className='advanced-options settings-section'>
|
||||
{this.renderVersionOptions()}
|
||||
<Callout>
|
||||
{this.renderVersionChannelOptions()}
|
||||
{this.renderVersionStateOptions()}
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
{this.renderAdvancedButtons()}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Versions</h2>
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -150,46 +160,78 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
*/
|
||||
private renderAdvancedButtons(): JSX.Element {
|
||||
const { isDownloadingAll, isDeletingAll } = this.state;
|
||||
const { isUpdatingElectronVersions } = this.props.appState;
|
||||
const isWorking = isDownloadingAll || isDeletingAll;
|
||||
const downloadFontAwesomeIcon = isDownloadingAll
|
||||
? <FontAwesomeIcon icon={faSpinner} spin={true} />
|
||||
: <FontAwesomeIcon icon={faCloudDownloadAlt} />;
|
||||
const deleteFontAwesomeIcon = isDeletingAll
|
||||
? <FontAwesomeIcon icon={faSpinner} spin={true} />
|
||||
: <FontAwesomeIcon icon={faTrash} />;
|
||||
|
||||
return (
|
||||
<div className='advanced-options-buttons'>
|
||||
<div>
|
||||
<label>Download all versions of Electron.</label>
|
||||
<button
|
||||
className='button btn-download-all'
|
||||
disabled={isWorking}
|
||||
onClick={this.handleDownloadAll}
|
||||
>
|
||||
{downloadFontAwesomeIcon} Download All Versions
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label>Remove all downloaded versions.</label>
|
||||
<button
|
||||
className='button btn-delete-all'
|
||||
disabled={isWorking}
|
||||
onClick={this.handleDeleteAll}
|
||||
>
|
||||
{deleteFontAwesomeIcon} Delete All Downloads
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label>Add a local build of Electron.</label>
|
||||
<button
|
||||
className='button btn-add-version'
|
||||
onClick={this.handleAddVersion}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFile} /> Add Local Electron Build
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup fill={true}>
|
||||
<Button
|
||||
disabled={isUpdatingElectronVersions}
|
||||
onClick={this.handleDownloadClick}
|
||||
loading={isUpdatingElectronVersions}
|
||||
icon='numbered-list'
|
||||
text='Update Electron Release List'
|
||||
/>
|
||||
<Button
|
||||
disabled={isWorking}
|
||||
icon='download'
|
||||
onClick={this.handleDownloadAll}
|
||||
text='Download All Versions'
|
||||
/>
|
||||
<Button
|
||||
disabled={isWorking}
|
||||
icon='trash'
|
||||
onClick={this.handleDeleteAll}
|
||||
text='Delete All Downloads'
|
||||
/>
|
||||
<Button
|
||||
icon='document-open'
|
||||
onClick={this.handleAddVersion}
|
||||
text='Add Local Electron Build'
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the various options for which versions should be displayed
|
||||
* in the small dropdown.
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderVersionStateOptions(): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const getIsChecked = (state: ElectronVersionState) => {
|
||||
return appState.statesToShow.includes(state);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label='Include Electron versions that are:'
|
||||
>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronVersionState.downloading)}
|
||||
label='Downloading'
|
||||
id='downloading'
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronVersionState.ready)}
|
||||
label='Downloaded'
|
||||
id='ready'
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronVersionState.unknown)}
|
||||
label='Not Downloaded'
|
||||
id='unknown'
|
||||
onChange={this.handleStateChange}
|
||||
inline={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,91 +241,45 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderVersionOptions(): JSX.Element {
|
||||
private renderVersionChannelOptions(): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const getIsChecked = (channel: ElectronReleaseChannel) => {
|
||||
return appState.versionsToShow.includes(channel);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='versions-to-show'>
|
||||
<p>Include Electron versions from these release channels:</p>
|
||||
<label>
|
||||
<input
|
||||
checked={getIsChecked(ElectronReleaseChannel.stable)}
|
||||
id={ElectronReleaseChannel.stable}
|
||||
type='checkbox'
|
||||
onChange={this.handleChannelChange}
|
||||
/>
|
||||
Stable
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
checked={getIsChecked(ElectronReleaseChannel.beta)}
|
||||
id={ElectronReleaseChannel.beta}
|
||||
type='checkbox'
|
||||
onChange={this.handleChannelChange}
|
||||
/>
|
||||
Beta
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
checked={getIsChecked(ElectronReleaseChannel.nightly)}
|
||||
id={ElectronReleaseChannel.nightly}
|
||||
type='checkbox'
|
||||
onChange={this.handleChannelChange}
|
||||
/>
|
||||
Nightly
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
checked={getIsChecked(ElectronReleaseChannel.unsupported)}
|
||||
id={ElectronReleaseChannel.unsupported}
|
||||
type='checkbox'
|
||||
onChange={this.handleChannelChange}
|
||||
/>
|
||||
Unsupported
|
||||
</label>
|
||||
<p>Number of recent Electron releases to include:</p>
|
||||
<select onChange={this.handlePagesChange}>
|
||||
<option value='1'>30</option>
|
||||
<option value='2'>60</option>
|
||||
<option value='3'>90</option>
|
||||
<option value='4'>120</option>
|
||||
<option value='5'>150</option>
|
||||
<option value='6'>180</option>
|
||||
<option value='7'>210</option>
|
||||
<option value='8'>240</option>
|
||||
<option value='9'>270</option>
|
||||
<option value='10'>300</option>
|
||||
</select>
|
||||
<br />
|
||||
{this.renderUpdateVersionsButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the "Update Electron Release List" button
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderUpdateVersionsButton(): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const { isUpdatingElectronVersions } = appState;
|
||||
const icon = isUpdatingElectronVersions
|
||||
? <FontAwesomeIcon icon={faSpinner} spin={true}/>
|
||||
: <FontAwesomeIcon icon={faClipboardList} />;
|
||||
|
||||
return (
|
||||
<button
|
||||
className='button'
|
||||
disabled={isUpdatingElectronVersions}
|
||||
onClick={this.handleDownloadClick}
|
||||
<FormGroup
|
||||
label='Include Electron versions from these release channels:'
|
||||
>
|
||||
{icon} Update Electron Release List
|
||||
</button>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.stable)}
|
||||
label='Stable'
|
||||
id='Stable'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.beta)}
|
||||
label='Beta'
|
||||
id='Beta'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.nightly)}
|
||||
label='Nightly'
|
||||
id='Nightly'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.unsupported)}
|
||||
label='Unsupported'
|
||||
id='Unsupported'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,7 +291,10 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
*/
|
||||
private renderTable(): JSX.Element {
|
||||
return (
|
||||
<table className='electron-versions-table'>
|
||||
<HTMLTable
|
||||
className='electron-versions-table'
|
||||
striped={true}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
@@ -306,7 +305,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
<tbody>
|
||||
{this.renderTableRows()}
|
||||
</tbody>
|
||||
</table>
|
||||
</HTMLTable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -317,7 +316,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
private renderTableRows(): Array<JSX.Element | null> {
|
||||
const { versions, versionsToShow } = this.props.appState;
|
||||
const { versions, versionsToShow, statesToShow } = this.props.appState;
|
||||
|
||||
return sortedElectronMap<JSX.Element | null>(versions, (key, item) => {
|
||||
// Check if we want to show the version
|
||||
@@ -325,21 +324,47 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
return null;
|
||||
}
|
||||
|
||||
const { tag_name, state } = item;
|
||||
const humanState = state === 'ready'
|
||||
? '✅ Downloaded'
|
||||
: state === 'downloading' ? '⏬ Downloading' : '⏹ Not downloaded';
|
||||
// Check if we want to show the state
|
||||
if (!statesToShow.includes(item.state)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={tag_name}>
|
||||
<td>{tag_name}</td>
|
||||
<td>{humanState}</td>
|
||||
<td>{this.renderAction(key, item)}</td>
|
||||
<tr key={item.version}>
|
||||
<td>{item.version}</td>
|
||||
<td>{this.renderHumanState(item)}</td>
|
||||
<td className='action'>{this.renderAction(key, item)}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable state indicator for an Electron version
|
||||
*
|
||||
* @param {ElectronVersion} item
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderHumanState(item: ElectronVersion): JSX.Element {
|
||||
const { state } = item;
|
||||
let icon: IconName = 'box';
|
||||
let humanState = 'Downloaded';
|
||||
|
||||
if (state === ElectronVersionState.downloading) {
|
||||
icon = 'cloud-download';
|
||||
humanState = 'Downloading';
|
||||
} else if (state === ElectronVersionState.unknown) {
|
||||
icon = 'cloud';
|
||||
humanState = 'Not downloaded';
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon={icon} /> {humanState}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the action for a single Electron version
|
||||
*
|
||||
@@ -351,37 +376,31 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
private renderAction(key: string, item: ElectronVersion): JSX.Element {
|
||||
const { state, source } = item;
|
||||
const { appState } = this.props;
|
||||
const buttonProps: IButtonProps = {
|
||||
fill: true,
|
||||
small: true
|
||||
};
|
||||
|
||||
// Already downloaded
|
||||
if (state === 'ready') {
|
||||
const remove = () => appState.removeVersion(key);
|
||||
const label = source === ElectronVersionSource.local
|
||||
buttonProps.onClick = () => appState.removeVersion(key);
|
||||
buttonProps.icon = 'trash';
|
||||
buttonProps.text = source === ElectronVersionSource.local
|
||||
? 'Remove'
|
||||
: 'Delete';
|
||||
|
||||
return (
|
||||
<button className='button' onClick={remove}>
|
||||
<FontAwesomeIcon icon={faTrash} /> {label}
|
||||
</button>
|
||||
);
|
||||
} 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);
|
||||
}
|
||||
|
||||
// Downloading
|
||||
if (state === 'downloading') {
|
||||
return (
|
||||
<button className='button' disabled={true}>
|
||||
<FontAwesomeIcon icon={faSpinner} spin={true}/> Downloading
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Unknown
|
||||
const download = () => appState.setVersion(key);
|
||||
|
||||
return (
|
||||
<button className='button' onClick={download}>
|
||||
<FontAwesomeIcon icon={faCloudDownloadAlt} /> Download
|
||||
</button>
|
||||
);
|
||||
return <Button {...buttonProps} type={undefined} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Callout, Checkbox, FormGroup } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ExecutionSettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage execution-related preferences.
|
||||
*
|
||||
* @class ExecutionSettings
|
||||
* @extends {React.Component<ExecutionSettingsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {}> {
|
||||
constructor(props: ExecutionSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.handleDeleteDataChange = this.handleDeleteDataChange.bind(this);
|
||||
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change on whether or not the user data dir should be deleted
|
||||
* after a run.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleDeleteDataChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { checked } = event.currentTarget;
|
||||
this.props.appState.isKeepingUserDataDirs = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change on whether or not electron should log more things
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleElectronLoggingChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { checked } = event.currentTarget;
|
||||
this.props.appState.isEnablingElectronLogging = checked;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isKeepingUserDataDirs, isEnablingElectronLogging } = this.props.appState;
|
||||
|
||||
const deleteUserDirLabel = `
|
||||
Whenever Electron runs, it creates a user data directory for cookies, the cache,
|
||||
and various other things that it needs to keep around. Since fiddles are usually
|
||||
just run once, we delete this directory after your fiddle exits. Enable this
|
||||
setting to keep the user data directories around.
|
||||
`.trim();
|
||||
const electronLoggingLabel = `
|
||||
There are some flags that Electron uses to log extra information both internally
|
||||
and through Chromium. Enable this option to make Fiddle produce those logs.`.trim();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Exection</h2>
|
||||
<Callout>
|
||||
These advanced settings control how Electron Fiddle executes your fiddles.
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
<FormGroup label={deleteUserDirLabel}>
|
||||
<Checkbox
|
||||
checked={isKeepingUserDataDirs}
|
||||
label='Do not delete user data directories.'
|
||||
onChange={this.handleDeleteDataChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
<br />
|
||||
<Callout>
|
||||
<FormGroup label={electronLoggingLabel}>
|
||||
<Checkbox
|
||||
checked={isEnablingElectronLogging}
|
||||
label='Enable advanced Electron logging.'
|
||||
onChange={this.handleElectronLoggingChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,64 @@
|
||||
import { Button, Callout, FormGroup, MenuItem } from '@blueprintjs/core';
|
||||
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
|
||||
import { shell } from 'electron';
|
||||
import * as fsType from 'fs-extra';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
|
||||
import { highlightText } from '../../utils/highlight-text';
|
||||
import { fancyImport } from '../../utils/import';
|
||||
import { AppState } from '../state';
|
||||
import { getAvailableThemes, getTheme, THEMES_PATH } from '../themes';
|
||||
import { LoadedFiddleTheme } from '../themes-defaults';
|
||||
|
||||
const ThemeSelect = Select.ofType<LoadedFiddleTheme>();
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> predicate for an Electron
|
||||
* version.
|
||||
*
|
||||
* @param {string} query
|
||||
* @param {ElectronVersion} { version }
|
||||
* @returns
|
||||
*/
|
||||
export const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
|
||||
return name.toLowerCase().includes(query.toLowerCase());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Helper method: Returns the <Select /> <MenuItem /> for Electron
|
||||
* versions.
|
||||
*
|
||||
* @param {ElectronVersion} item
|
||||
* @param {IItemRendererProps} { handleClick, modifiers, query }
|
||||
* @returns
|
||||
*/
|
||||
export const renderItem: ItemRenderer<LoadedFiddleTheme> = (item, { handleClick, modifiers, query }) => {
|
||||
if (!modifiers.matchesPredicate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
text={highlightText(item.name, query)}
|
||||
key={item.name}
|
||||
onClick={handleClick}
|
||||
icon='media'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface AppearanceSettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface AppearanceSettingsState {
|
||||
themes: Array<LoadedFiddleTheme>;
|
||||
selectedTheme?: LoadedFiddleTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,18 +82,26 @@ export class AppearanceSettings extends React.Component<
|
||||
};
|
||||
|
||||
getAvailableThemes().then((themes) => {
|
||||
this.setState({ 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 {React.ChangeEvent<HTMLSelectElement>} event
|
||||
* @param {LoadedFiddleTheme} theme
|
||||
*/
|
||||
public handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
this.props.appState.setTheme(event.target.value);
|
||||
public handleChange(theme: LoadedFiddleTheme) {
|
||||
this.setState({ selectedTheme: theme });
|
||||
this.props.appState.setTheme(theme.file);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +126,7 @@ export class AppearanceSettings extends React.Component<
|
||||
css: undefined
|
||||
}, { spaces: 2 });
|
||||
|
||||
shell.openItem(themePath);
|
||||
shell.showItemInFolder(themePath);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -103,48 +155,47 @@ export class AppearanceSettings extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the theme options.
|
||||
*
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
public renderOptions(): Array<JSX.Element> {
|
||||
const { themes } = this.state;
|
||||
|
||||
return themes.map(({ name, file }) => {
|
||||
return (
|
||||
<option value={file} key={file}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { selectedTheme } = this.state;
|
||||
const selectedName = selectedTheme && selectedTheme.name || 'Select a theme';
|
||||
|
||||
return (
|
||||
<div className='settings-appearance'>
|
||||
<h4>Appearance</h4>
|
||||
<label key='theme-label'>
|
||||
To add themes, add JSON theme files to <a
|
||||
id='open-theme-folder'
|
||||
onClick={() => this.openThemeFolder()}
|
||||
<FormGroup
|
||||
label='Choose your theme'
|
||||
inline={true}
|
||||
>
|
||||
<ThemeSelect
|
||||
filterable={true}
|
||||
items={this.state.themes}
|
||||
itemRenderer={renderItem}
|
||||
itemPredicate={filterItem}
|
||||
onItemSelect={this.handleChange}
|
||||
noResults={<MenuItem disabled={true} text='No results.' />}
|
||||
>
|
||||
<code>{THEMES_PATH}</code>
|
||||
</a>.<br />
|
||||
</label>
|
||||
<select
|
||||
className='select-themes'
|
||||
value={`${this.props.appState.theme}`}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
{this.renderOptions()}
|
||||
</select>
|
||||
<button
|
||||
id='create-new-theme'
|
||||
onClick={() => this.createNewThemeFromCurrent()}
|
||||
>
|
||||
Create theme from current selection
|
||||
</button>
|
||||
<Button
|
||||
text={selectedName}
|
||||
icon='tint'
|
||||
/>
|
||||
</ThemeSelect>
|
||||
</FormGroup>
|
||||
<Callout>
|
||||
<p>
|
||||
To add themes, add JSON theme files to <a
|
||||
id='open-theme-folder'
|
||||
onClick={this.openThemeFolder}
|
||||
>
|
||||
<code>{THEMES_PATH}</code>
|
||||
</a>. The easiest way to get started is to clone one of the two existing
|
||||
themes and to add your own colors.
|
||||
</p>
|
||||
<Button
|
||||
onClick={this.createNewThemeFromCurrent}
|
||||
text='Create theme from current selection'
|
||||
icon='duplicate'
|
||||
/>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Callout, Checkbox, FormGroup } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface ConsoleSettingsProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings content to manage console-related preferences.
|
||||
*
|
||||
* @class ConsoleSettings
|
||||
* @extends {React.Component<ConsoleSettingsProps, {}>}
|
||||
*/
|
||||
@observer
|
||||
export class ConsoleSettings extends React.Component<ConsoleSettingsProps, {}> {
|
||||
constructor(props: ConsoleSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.handleClearOnRunChange = this.handleClearOnRunChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change on whether or not the console should be cleared
|
||||
* before fiddle is executed.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleClearOnRunChange(
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { checked } = event.currentTarget;
|
||||
this.props.appState.isClearingConsoleOnRun = checked;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isClearingConsoleOnRun } = this.props.appState;
|
||||
|
||||
const clearOnRunLabel = `
|
||||
Enable this option to automatically clear the console whenever you run your
|
||||
fiddle.`.trim();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>Console</h4>
|
||||
<Callout>
|
||||
<FormGroup label={clearOnRunLabel}>
|
||||
<Checkbox
|
||||
checked={isClearingConsoleOnRun}
|
||||
label='Clear on run.'
|
||||
onChange={this.handleClearOnRunChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { faSignInAlt, faSignOutAlt } from '@fortawesome/fontawesome-free-solid';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button, Callout } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
@@ -17,53 +16,48 @@ export interface GitHubSettingsProps {
|
||||
*/
|
||||
@observer
|
||||
export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
|
||||
constructor(props: GitHubSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.signIn = this.signIn.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "logged out" settings experience.
|
||||
*
|
||||
* @returns {Array<JSX.Element>}
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
public renderNotSignedIn(): Array<JSX.Element> {
|
||||
const signIn = () => {
|
||||
this.props.appState.isTokenDialogShowing = true;
|
||||
};
|
||||
|
||||
return [
|
||||
(
|
||||
<label key='signed-out-label'>
|
||||
We can publish your fiddles to GitHub as a Gist -
|
||||
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!
|
||||
</label>
|
||||
),
|
||||
(
|
||||
<button className='button' key='sign-in-button' onClick={signIn}>
|
||||
<FontAwesomeIcon icon={faSignInAlt} /> Sign In
|
||||
</button>
|
||||
)
|
||||
];
|
||||
</p>
|
||||
<Button onClick={this.signIn} icon='log-in' text='Sign in'/>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "logged in" settings experience.
|
||||
*
|
||||
* @returns {Array<JSX.Element>}
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
public renderSignedIn(): Array<JSX.Element> {
|
||||
public renderSignedIn(): JSX.Element {
|
||||
const { gitHubLogin } = this.props.appState;
|
||||
const signOut = this.props.appState.signOutGitHub;
|
||||
|
||||
return [
|
||||
(
|
||||
<label key='signed-in-label'>
|
||||
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 {gitHubLogin}.
|
||||
</label>
|
||||
),
|
||||
(
|
||||
<button className='button' key='sign-out-button' onClick={signOut}>
|
||||
<FontAwesomeIcon icon={faSignOutAlt} /> Remove Token & Sign Out
|
||||
</button>
|
||||
)
|
||||
];
|
||||
logged you into GitHub as <code>{gitHubLogin}</code>.
|
||||
</p>
|
||||
<Button onClick={signOut} icon='log-out' text='Sign out'/>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
@@ -80,4 +74,11 @@ export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply shows the GitHub Token dialog.``
|
||||
*/
|
||||
private signIn() {
|
||||
this.props.appState.isTokenDialogShowing = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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 {
|
||||
@@ -22,7 +24,9 @@ export class GeneralSettings extends React.Component<GeneralSettingsProps, {}> {
|
||||
<div>
|
||||
<h2>General Settings</h2>
|
||||
<AppearanceSettings appState={this.props.appState} />
|
||||
<hr />
|
||||
<Divider />
|
||||
<ConsoleSettings appState={this.props.appState} />
|
||||
<Divider />
|
||||
<GitHubSettings appState={this.props.appState} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { faTimesCircle } from '@fortawesome/fontawesome-free-solid';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Icon, IconName, MenuItem } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { classNames } from '../../utils/classnames';
|
||||
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
|
||||
];
|
||||
|
||||
@@ -63,6 +64,10 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
|
||||
return <ElectronSettings appState={appState} />;
|
||||
}
|
||||
|
||||
if (section === SettingsSections.Execution) {
|
||||
return <ExecutionSettings appState={appState} />;
|
||||
}
|
||||
|
||||
if (section === SettingsSections.Credits) {
|
||||
return <CreditsSettings appState={appState} />;
|
||||
}
|
||||
@@ -80,11 +85,17 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
|
||||
|
||||
return settingsSections.map((name) => {
|
||||
const isSelected = section === name;
|
||||
const className = classNames({ selected: isSelected }, name);
|
||||
const onClick = () => this.setState({ section: name });
|
||||
|
||||
return (
|
||||
<li onClick={onClick} key={name} className={className}>{name}</li>
|
||||
<MenuItem
|
||||
onClick={onClick}
|
||||
active={isSelected}
|
||||
key={name}
|
||||
id={`settings-link-${name}`}
|
||||
text={name}
|
||||
icon={this.getIconForSection(name)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -102,11 +113,29 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
|
||||
</div>
|
||||
<div className='settings-content'>
|
||||
<div className='settings-close' onClick={appState.toggleSettings}>
|
||||
<FontAwesomeIcon icon={faTimesCircle} />
|
||||
<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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { DOCS_DEMO_COMPONENTS } from './show-me/index';
|
||||
|
||||
export interface ShowMeProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**3
|
||||
* The root component for "show me" content - the fourth helpful
|
||||
* panel.
|
||||
*
|
||||
* @class ShowMe
|
||||
* @extends {React.Component<ShowMeProps, ShowMeState>}
|
||||
*/
|
||||
@observer
|
||||
export class ShowMe extends React.Component<ShowMeProps, {}> {
|
||||
public render() {
|
||||
const { currentDocsDemoPage: showMeName } = this.props.appState;
|
||||
const Content = DOCS_DEMO_COMPONENTS[showMeName] || DOCS_DEMO_COMPONENTS.DEFAULT;
|
||||
|
||||
return (
|
||||
<div className='show-me-panel'>
|
||||
<Content appState={this.props.appState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, Callout, Icon } from '@blueprintjs/core';
|
||||
import { remote } from 'electron';
|
||||
|
||||
import { renderMoreDocumentation } from './more-information';
|
||||
import { getSubsetOnly } from './subset-only';
|
||||
|
||||
/**
|
||||
* Show and hide button examples, only rendered on macOS
|
||||
*
|
||||
* @returns {JSX.Element | null}
|
||||
*/
|
||||
function ShowHide() {
|
||||
if (process.platform !== 'darwin') return null;
|
||||
|
||||
const onClick = () => {
|
||||
remote.app.hide();
|
||||
setTimeout(() => {
|
||||
remote.app.show();
|
||||
remote.app.focus();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>This button hides Electron Fiddle right away, showing it again in two seconds.</p>
|
||||
<Button
|
||||
id='show-hide'
|
||||
icon='eye-off'
|
||||
text='Hide Electron Fiddle'
|
||||
onClick={onClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export function ShowMeApp(_props: any): JSX.Element {
|
||||
const [ secondsLeft, setSeconds ] = React.useState(-1);
|
||||
|
||||
const playFocus = () => {
|
||||
setSeconds(3);
|
||||
setTimeout(() => setSeconds(2), 1000);
|
||||
setTimeout(() => setSeconds(1), 2000);
|
||||
setTimeout(() => {
|
||||
remote.app.focus();
|
||||
setSeconds(-1);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const [ paths, setPaths ] = React.useState('');
|
||||
const playPaths = () => {
|
||||
const pathsToQuery = [
|
||||
'home',
|
||||
'appData',
|
||||
'userData',
|
||||
'temp',
|
||||
'downloads',
|
||||
'desktop'
|
||||
];
|
||||
|
||||
let result = '';
|
||||
pathsToQuery.forEach((item) => {
|
||||
result += `${item}: ${remote.app.getPath(item)}\n`;
|
||||
});
|
||||
|
||||
setPaths(result);
|
||||
};
|
||||
|
||||
const [ metrics, setMetrics ] = React.useState('');
|
||||
const playMetrics = () => {
|
||||
setMetrics(JSON.stringify(remote.app.getAppMetrics(), undefined, 2));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon icon='help' iconSize={40} style={{ float: 'left', margin: '0 10px 0 0' }} />
|
||||
<p className='bp3r-running-text'>
|
||||
The <code>app</code> module controls the app's application life-cycle. Most of the
|
||||
events and methods available on this module are responsible for handling how your
|
||||
interacts with the operating system or to set application-wide settings.
|
||||
</p>
|
||||
<h3>API Demos</h3>
|
||||
{getSubsetOnly('app')}
|
||||
<Callout
|
||||
title='Hiding, Showing, Focussing'
|
||||
icon='eye-open'
|
||||
>
|
||||
<p>
|
||||
The app can ask the operating system for window focus. On macOS, it can additionally
|
||||
request that the app be hidden or shown. Give it a try: Click on the button below,
|
||||
focus another app, and wait for two seconds to see Electron Fiddle become the focused
|
||||
app again.
|
||||
</p>
|
||||
<Button
|
||||
id='focus'
|
||||
icon='lightbulb'
|
||||
text={`Focus Electron Fiddle${secondsLeft > 0 ? ` in ${secondsLeft}s` : ''}`}
|
||||
onClick={playFocus}
|
||||
/>
|
||||
<ShowHide />
|
||||
</Callout>
|
||||
<Callout
|
||||
title='Paths'
|
||||
icon='folder-open'
|
||||
>
|
||||
<p>
|
||||
Need to query information about various paths in a cross-platform manner? Electron
|
||||
can help. The button queries the operating system for some of them.
|
||||
</p>
|
||||
<Button
|
||||
id='special-paths'
|
||||
icon='play'
|
||||
text={`Get special directory paths`}
|
||||
onClick={playPaths}
|
||||
/>
|
||||
<pre id='special-paths-content'>
|
||||
{paths}
|
||||
</pre>
|
||||
</Callout>
|
||||
<Callout
|
||||
title='Process & Device Information'
|
||||
icon='pulse'
|
||||
>
|
||||
<p>
|
||||
Need to query information about the process or system? The <code>app</code> module lets
|
||||
developers query for information about the running app, hardware, and operating system.
|
||||
A good example are process metrics.
|
||||
</p>
|
||||
<Button
|
||||
id='process-metrics'
|
||||
icon='play'
|
||||
text={`Get process metrics`}
|
||||
onClick={playMetrics}
|
||||
/>
|
||||
<pre id='process-metrics-content'>
|
||||
{metrics}
|
||||
</pre>
|
||||
</Callout>
|
||||
{renderMoreDocumentation()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Button, Icon } from '@blueprintjs/core';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DocsDemoPage } from '../../../interfaces';
|
||||
import { AppState } from '../../state';
|
||||
import { DOCS_DEMO_COMPONENTS, DOCS_DEMO_NAMES } from './index';
|
||||
import { renderMoreDocumentation } from './more-information';
|
||||
|
||||
export function ShowMeDefault(props: { appState: AppState }): JSX.Element {
|
||||
const showMeMenu: Array<JSX.Element> = Object.keys(DOCS_DEMO_COMPONENTS)
|
||||
.filter((key) => key !== DocsDemoPage.DEFAULT)
|
||||
.map((key) => {
|
||||
return (
|
||||
<li key={key}>
|
||||
<Button
|
||||
minimal={true}
|
||||
icon='play'
|
||||
text={DOCS_DEMO_NAMES[key]}
|
||||
onClick={() => (props.appState.currentDocsDemoPage = key as DocsDemoPage)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon icon='help' iconSize={40} style={{ float: 'left', margin: '0 10px 0 0' }} />
|
||||
<p className='bp3r-running-text'>
|
||||
This panel offers useful information about Electron APIs – and
|
||||
easy way to try some of the methods it offers. Fiddle has an example fiddle
|
||||
for every module available in Electron.
|
||||
</p>
|
||||
<p>
|
||||
Clicking on any of the modules below will open up a fiddle showcasing
|
||||
that particular module.
|
||||
</p>
|
||||
<ul className='show-me-list'>
|
||||
{...showMeMenu}
|
||||
</ul>
|
||||
{renderMoreDocumentation()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DocsDemoPage } from '../../../interfaces';
|
||||
|
||||
import { ShowMeApp } from './app';
|
||||
import { ShowMeDefault } from './default';
|
||||
|
||||
export const DOCS_DEMO_COMPONENTS: Record<DocsDemoPage, any> = {
|
||||
DEFAULT: ShowMeDefault,
|
||||
DEMO_APP: ShowMeApp
|
||||
};
|
||||
|
||||
export const DOCS_DEMO_NAMES: Record<DocsDemoPage, string> = {
|
||||
DEFAULT: 'Home',
|
||||
DEMO_APP: 'App Demos'
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { shell } from 'electron';
|
||||
|
||||
/**
|
||||
* Renders a "more documentation" block, typically used at the end of a "show me" documentation piece.
|
||||
*
|
||||
* @export
|
||||
* @param {string} [url='electronjs.org/docs']
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function renderMoreDocumentation(
|
||||
url: string = 'electronjs.org/docs'
|
||||
): JSX.Element {
|
||||
const fullUrl = `https://${url}`;
|
||||
const gitHubUrl = `https://github.com/electron/fiddle`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<p className='b3-running-text'>
|
||||
For more documentation, visit <a id='open-url' onClick={() => shell.openExternal(fullUrl)}>{url}</a>, where
|
||||
you can find the full documentation for Electron.
|
||||
</p>
|
||||
<p className='bp3-text-muted'>
|
||||
By the way, Electron Fiddle and the documentation you see here is entirely open source. If you
|
||||
have ideas on how to improve it, we'd love to have your contributions! You can find the
|
||||
repository on <a id='open-github' onClick={() => shell.openExternal(gitHubUrl)}>GitHub</a>.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { shell } from 'electron';
|
||||
|
||||
import { getDocsUrlForModule } from '../../../utils/docs-urls';
|
||||
|
||||
/**
|
||||
* Returns a common string we use as a disclaimer to explain that this set of demos
|
||||
* is only meant to be a short demonstration, not displaying all of the modules
|
||||
* abilities.
|
||||
*
|
||||
* @export
|
||||
* @param {string} moduleName
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function getSubsetOnly(moduleName: string): JSX.Element {
|
||||
const { full, short } = getDocsUrlForModule(moduleName);
|
||||
|
||||
return (
|
||||
<p className='bp3-running-text'>
|
||||
The following demos display only a subset of what the <code>{moduleName}</code>
|
||||
module is capable of. If you want to see its full abilities, check out the
|
||||
documentation on <a id='open-url' onClick={() => shell.openExternal(full)}>{short}</a>.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { Dialog } from './dialog';
|
||||
import { Tour, TourScriptStep, TourStepGetButtonParams } from './tour';
|
||||
|
||||
export interface WelcomeTourProps {
|
||||
@@ -19,14 +19,14 @@ export interface WelcomeTourState {
|
||||
*
|
||||
* @returns {Set<TourScriptStep>}
|
||||
*/
|
||||
function getWelcomeTour(): Set<TourScriptStep> {
|
||||
export function getWelcomeTour(): Set<TourScriptStep> {
|
||||
return new Set([
|
||||
{
|
||||
name: 'fiddle-editors',
|
||||
selector: 'div.mosaic-root',
|
||||
title: '📝 Fiddle Editors',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📝 Fiddle Editors</h4>
|
||||
<>
|
||||
<p>
|
||||
Electron Fiddle allows you to build little experiments and mini-apps with
|
||||
Electron. Each Fiddle has three files: A main script, a renderer script,
|
||||
@@ -37,15 +37,15 @@ function getWelcomeTour(): Set<TourScriptStep> {
|
||||
it automatically. It will also automatically provide you with autocomplete
|
||||
information for the <code>electron</code> module.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'select-versions',
|
||||
selector: 'select.select-versions',
|
||||
selector: 'button.version-chooser',
|
||||
title: '📇 Choose an Electron Version',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📇 Choose an Electron Version</h4>
|
||||
<>
|
||||
<p>
|
||||
Electron Fiddle knows about all released Electron versions, downloading
|
||||
your versions automatically in the background.
|
||||
@@ -54,27 +54,25 @@ function getWelcomeTour(): Set<TourScriptStep> {
|
||||
Open the preferences to see all available versions and delete those previously
|
||||
downloaded.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'button-run',
|
||||
selector: 'button.button-run',
|
||||
selector: '.button-run',
|
||||
title: '🚀 Run Your Fiddle',
|
||||
content: (
|
||||
<div>
|
||||
<h4>🚀 Run Your Fiddle</h4>
|
||||
<p>
|
||||
Hit this button to give your Fiddle a try and start it.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
Hit this button to give your Fiddle a try and start it.
|
||||
</p>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'button-publish',
|
||||
selector: 'button.button-publish',
|
||||
selector: '.button-publish',
|
||||
title: '🗺 Share Your Fiddle',
|
||||
content: (
|
||||
<div>
|
||||
<h4>🗺 Share Your Fiddle</h4>
|
||||
<>
|
||||
<p>
|
||||
Like what you've built? You can save your Fiddle as a public GitHub Gist,
|
||||
allowing other users to load it by pasting the URL into the address bar.
|
||||
@@ -85,41 +83,39 @@ function getWelcomeTour(): Set<TourScriptStep> {
|
||||
You can also package your Fiddle as a standalone binary or as an installer
|
||||
from the "Tasks" menu.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'first-time-electron',
|
||||
selector: 'div.mosaic-root',
|
||||
title: '👋 Getting Started With Electron?',
|
||||
content: (
|
||||
<div>
|
||||
<h4>👋 Getting Started With Electron?</h4>
|
||||
<p>
|
||||
We've finished our tour of Electron Fiddle, but if this is your
|
||||
first time using Electron, we could introduce you to its basics.
|
||||
Interested?
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
We've finished our tour of Electron Fiddle, but if this is your
|
||||
first time using Electron, we could introduce you to its basics.
|
||||
Interested?
|
||||
</p>
|
||||
),
|
||||
getButtons: ({ stop, advance }: TourStepGetButtonParams): Array<JSX.Element> => {
|
||||
return [
|
||||
<button key='btn-stop' onClick={stop}>I'm good!</button>,
|
||||
<button key='btn-adv' onClick={advance}>Electron Basics</button>
|
||||
<Button key='btn-stop' onClick={stop} text="I'm good!" icon='stop' />,
|
||||
<Button key='btn-adv' onClick={advance} text='Electron Basics' icon='help' />
|
||||
];
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'main-editor',
|
||||
selector: 'div.mosaic-window.main',
|
||||
title: '📝 Main Script',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📝 Main Script</h4>
|
||||
<>
|
||||
<p>
|
||||
Every Electron app starts with a main script, very similar to how
|
||||
a Node.js application is started. The main script runs in the "main
|
||||
process". To display a user interface, the main process creates renderer
|
||||
processes – usually in the form of windows, which Electron calls
|
||||
<code>BrowserWindow</code>.
|
||||
<code>BrowserWindow</code>.
|
||||
</p>
|
||||
<p>
|
||||
To get started, pretend that the main process is just like a Node.js
|
||||
@@ -131,32 +127,30 @@ function getWelcomeTour(): Set<TourScriptStep> {
|
||||
The default fiddle creates a new <code>BrowserWindow</code> and loads
|
||||
an HTML file.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'html-editor',
|
||||
selector: 'div.mosaic-window.html',
|
||||
title: '📝 HTML',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📝 HTML</h4>
|
||||
<p>
|
||||
In the default fiddle, this HTML file is loaded in the
|
||||
<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
|
||||
in a browser will work here, too. In addition, Electron allows you
|
||||
to execute Node.js code. Take a close look at the
|
||||
<code><script /></code> tag and notice how we can call <code>
|
||||
require()</code> like we would in Node.js.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
In the default fiddle, this HTML file is loaded in the
|
||||
<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
|
||||
in a browser will work here, too. In addition, Electron allows you
|
||||
to execute Node.js code. Take a close look at the
|
||||
<code><script /></code> tag and notice how we can call <code>
|
||||
require()</code> like we would in Node.js.
|
||||
</p>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'renderer-editor',
|
||||
selector: 'div.mosaic-window.renderer',
|
||||
title: '📝 Renderer Script',
|
||||
content: (
|
||||
<div>
|
||||
<h4>📝 Renderer Script</h4>
|
||||
<>
|
||||
<p>
|
||||
This is the script we just required from the HTML file. In here, you can
|
||||
do anything that works in Node.js <i>and</i> anything that works in a browser.
|
||||
@@ -166,7 +160,7 @@ function getWelcomeTour(): Set<TourScriptStep> {
|
||||
<code>require</code> it. Electron Fiddle will automatically detect that you
|
||||
requested a module and install it as soon as you run your fiddle.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
]);
|
||||
@@ -206,6 +200,25 @@ export class WelcomeTour extends React.Component<WelcomeTourProps, WelcomeTourSt
|
||||
this.setState({ isTourStarted: true });
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
key='cancel'
|
||||
onClick={this.stopTour}
|
||||
icon='cross'
|
||||
text={`I'll figure it out`}
|
||||
/>
|
||||
<Button
|
||||
key='ok'
|
||||
onClick={this.startTour}
|
||||
icon='presentation'
|
||||
text='Show me around'
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isTourShowing } = this.props.appState;
|
||||
const { isTourStarted } = this.state;
|
||||
@@ -216,21 +229,26 @@ export class WelcomeTour extends React.Component<WelcomeTourProps, WelcomeTourSt
|
||||
return (
|
||||
<Dialog
|
||||
key='welcome-tour-dialog'
|
||||
isCentered={true}
|
||||
isShowing={true}
|
||||
isShowingBackdrop={true}
|
||||
onConfirm={this.startTour}
|
||||
onClose={this.stopTour}
|
||||
isOpen={true}
|
||||
>
|
||||
<h4>🙋 Hey There!</h4>
|
||||
<p>
|
||||
Welcome to Electron Fiddle! If you're new to the app,
|
||||
we'd like to give you a brief tour of its features.
|
||||
</p>
|
||||
<p>
|
||||
We won't show this dialog again, but you can always
|
||||
find the tour in the Help menu.
|
||||
</p>
|
||||
<div className={Classes.DIALOG_HEADER}>
|
||||
<h4 className={Classes.HEADING}>🙋 Hey There!</h4>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<p>
|
||||
Welcome to Electron Fiddle! If you're new to the app,
|
||||
we'd like to give you a brief tour of its features.
|
||||
</p>
|
||||
<p>
|
||||
We won't show this dialog again, but you can always
|
||||
find the tour in the Help menu.
|
||||
</p>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
{this.buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import * as React from 'react';
|
||||
import { invertPosition, positionForRect } from '../../utils/position-for-rect';
|
||||
import { Dialog } from './dialog';
|
||||
|
||||
import { positionForRect } from '../../utils/position-for-rect';
|
||||
|
||||
export interface TourScriptStep {
|
||||
name: string;
|
||||
selector: string;
|
||||
title: string;
|
||||
content: JSX.Element;
|
||||
getButtons?: (handlers: TourStepGetButtonParams) => Array<JSX.Element>;
|
||||
}
|
||||
@@ -116,12 +118,29 @@ export class Tour extends React.Component<TourProps, TourState> {
|
||||
|
||||
// No? Fine! Are we at the end of the tour?
|
||||
return this.props.tour.size === this.state.i
|
||||
? [
|
||||
<button key='btn-stop' className='btn-stop' onClick={this.stop}>Finish Tour</button>
|
||||
]
|
||||
: [
|
||||
<button key='btn-stop' className='btn-stop' onClick={this.stop}>Stop Tour</button>,
|
||||
<button key='btn-adv' className='btn-adv' onClick={this.advance}>Continue</button>
|
||||
? [(
|
||||
<Button
|
||||
icon='tick-circle'
|
||||
onClick={this.stop}
|
||||
key='btn-stop'
|
||||
text='Finish Tour'
|
||||
/>
|
||||
)] : [
|
||||
(
|
||||
<Button
|
||||
icon='stop'
|
||||
key='btn-stop'
|
||||
onClick={this.stop}
|
||||
text='Stop Tour'
|
||||
/>
|
||||
), (
|
||||
<Button
|
||||
icon='step-forward'
|
||||
key='btn-adv'
|
||||
onClick={this.advance}
|
||||
text='Continue'
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -142,13 +161,31 @@ export class Tour extends React.Component<TourProps, TourState> {
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
width: size.width - margin,
|
||||
margin: `${margin}px`
|
||||
margin: `${margin}px`,
|
||||
zIndex: 1000,
|
||||
};
|
||||
const arrow = invertPosition(position.type);
|
||||
|
||||
return (
|
||||
<Dialog key={step.name} buttons={buttons} style={style} arrow={arrow}>
|
||||
{step.content}
|
||||
<Dialog
|
||||
key={step.name}
|
||||
isOpen={true}
|
||||
style={style}
|
||||
portalClassName='tour-portal'
|
||||
backdropProps={{ style: { visibility: 'hidden' }}}
|
||||
>
|
||||
<div className={Classes.DIALOG_HEADER}>
|
||||
<h4 className={Classes.HEADING}>
|
||||
{step.title}
|
||||
</h4>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
{step.content}
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -161,9 +198,7 @@ export class Tour extends React.Component<TourProps, TourState> {
|
||||
*/
|
||||
private getStep(step: TourScriptStep): JSX.Element | null {
|
||||
const { selector } = step;
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) return null;
|
||||
|
||||
const element = document.querySelector(selector) || document.body!;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const { top, left, width, height } = rect;
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ElectronVersionSource } from '../../interfaces';
|
||||
import { sortedElectronMap } from '../../utils/sorted-electron-map';
|
||||
import { AppState } from '../state';
|
||||
import { getReleaseChannel } from '../versions';
|
||||
|
||||
export interface VersionChooserState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface VersionChooserProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dropdown allowing the selection of Electron versions. The actual
|
||||
* download is managed in the state.
|
||||
*
|
||||
* @class VersionChooser
|
||||
* @extends {React.Component<VersionChooserProps, VersionChooserState>}
|
||||
*/
|
||||
@observer
|
||||
export class VersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
|
||||
constructor(props: VersionChooserProps) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle change, which usually means that we'd like update
|
||||
* the selection version.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLSelectElement>} event
|
||||
*/
|
||||
public handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
this.props.appState.setVersion(event.target.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the individual options (Electron versions)
|
||||
*
|
||||
* @returns {Array<JSX.Element>}
|
||||
*/
|
||||
public renderOptions(): Array<JSX.Element | null> {
|
||||
const { versions, versionsToShow } = this.props.appState;
|
||||
|
||||
return sortedElectronMap<JSX.Element | null>(versions, (_key, item) => {
|
||||
// Check if we want to show the version
|
||||
if (!item || !versionsToShow.includes(getReleaseChannel(item))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { tag_name, state, source, name } = item;
|
||||
const version = tag_name;
|
||||
const icon = state === 'ready'
|
||||
? '✅'
|
||||
: state === 'downloading' ? '⏬' : '⏹';
|
||||
const sourceInfo = source === ElectronVersionSource.local
|
||||
? `(${name})`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<option value={version} key={version}>
|
||||
{icon} {version} {sourceInfo}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<select
|
||||
className='select-versions'
|
||||
value={`v${this.props.appState.version}`}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
{this.renderOptions()}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { remote } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { MosaicNode } from 'react-mosaic-component';
|
||||
|
||||
import { EditorId, MosaicId } from '../interfaces';
|
||||
|
||||
// Reminder: When testing, this file is mocked in tests/setup.js
|
||||
|
||||
export const USER_DATA_PATH = remote.app.getPath('userData');
|
||||
export const CONFIG_PATH = path.join(remote.app.getPath('home'), '.electron-fiddle');
|
||||
|
||||
export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
|
||||
direction: 'row',
|
||||
first: EditorId.main,
|
||||
second: {
|
||||
direction: 'column',
|
||||
first: EditorId.renderer,
|
||||
second: EditorId.html,
|
||||
}
|
||||
};
|
||||
|
||||
export const ELECTRON_ORG = 'electron';
|
||||
export const ELECTRON_REPO = 'electron';
|
||||
@@ -1,30 +1,26 @@
|
||||
export const enum ContentNames {
|
||||
HTML = 'html',
|
||||
RENDERER = 'renderer',
|
||||
MAIN = 'main'
|
||||
}
|
||||
import { EditorId } from '../interfaces';
|
||||
|
||||
/**
|
||||
* Returns expected content for a given name.
|
||||
*
|
||||
* @export
|
||||
* @param {ContentNames} name
|
||||
* @param {EditorId} name
|
||||
* @param {string} [version]
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getContent(
|
||||
name: ContentNames,
|
||||
version?: string
|
||||
name: EditorId,
|
||||
version?: string,
|
||||
): Promise<string> {
|
||||
if (name === ContentNames.HTML) {
|
||||
if (name === EditorId.html) {
|
||||
return (await import('../content/html')).html;
|
||||
}
|
||||
|
||||
if (name === ContentNames.RENDERER) {
|
||||
if (name === EditorId.renderer) {
|
||||
return (await import('../content/renderer')).renderer;
|
||||
}
|
||||
|
||||
if (name === ContentNames.MAIN) {
|
||||
if (name === EditorId.main) {
|
||||
// We currently only distinguish between loadFile
|
||||
// and loadURL. Todo: Properly version the quick-start.
|
||||
if (version && version.startsWith('1.')) {
|
||||
@@ -40,18 +36,18 @@ export async function getContent(
|
||||
/**
|
||||
* Did the content change?
|
||||
*
|
||||
* @param {ContentNames} name
|
||||
* @param {EditorId} name
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function isContentUnchanged(name: ContentNames): Promise<boolean> {
|
||||
export async function isContentUnchanged(name: EditorId): Promise<boolean> {
|
||||
if (!window.ElectronFiddle || !window.ElectronFiddle.app) return false;
|
||||
|
||||
const values = await window.ElectronFiddle.app.getValues({ include: false });
|
||||
|
||||
// Handle main case, which needs to check both possible versions
|
||||
if (name === ContentNames.MAIN) {
|
||||
const isChanged1x = await getContent(ContentNames.MAIN, '1.0') === values.main;
|
||||
const isChangedOther = await getContent(ContentNames.MAIN) === values.main;
|
||||
if (name === EditorId.main) {
|
||||
const isChanged1x = await getContent(EditorId.main, '1.0') === values.main;
|
||||
const isChangedOther = await getContent(EditorId.main) === values.main;
|
||||
|
||||
return isChanged1x || isChangedOther;
|
||||
} else {
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as fsType from 'fs-extra';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as path from 'path';
|
||||
|
||||
import { USER_DATA_PATH } from '../constants';
|
||||
import { callIn } from '../utils/call-in';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { USER_DATA_PATH } from './constants';
|
||||
|
||||
const definitionPath = path.join(USER_DATA_PATH, 'electron-typedef');
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as fsType from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../constants';
|
||||
import { EditorValues, Files, FileTransform } from '../interfaces';
|
||||
import { EditorValues, Files, FileTransform, SetFiddleOptions } from '../interfaces';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../shared-constants';
|
||||
import { DEFAULT_OPTIONS, PackageJsonOptions } from '../utils/get-package';
|
||||
import { getTitle } from '../utils/get-title';
|
||||
import { fancyImport } from '../utils/import';
|
||||
@@ -35,13 +35,20 @@ export class FileManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a template.
|
||||
*
|
||||
* @param {string} name
|
||||
* @memberof FileManager
|
||||
*/
|
||||
public async openTemplate(name: string) {
|
||||
const values = await getTemplateValues(name);
|
||||
this.setFiddle(values);
|
||||
|
||||
this.setFiddle({ values, templateName: name });
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to open a fiddle
|
||||
* Tries to open a fiddle.
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @memberof FileManager
|
||||
@@ -59,19 +66,26 @@ export class FileManager {
|
||||
renderer: await this.readFile(path.join(filePath, RENDERER_JS_NAME)),
|
||||
};
|
||||
|
||||
this.setFiddle(values);
|
||||
this.setFiddle({ values, filePath });
|
||||
}
|
||||
|
||||
/**
|
||||
* Got values? This method will load them.
|
||||
*
|
||||
* @param {EditorValues} values
|
||||
* @param {SetFiddleOptions} options
|
||||
* @memberof FileManager
|
||||
*/
|
||||
public async setFiddle(values: EditorValues, filePath?: string) {
|
||||
public async setFiddle({ values, filePath, templateName }: SetFiddleOptions) {
|
||||
this.appState.gistId = '';
|
||||
this.appState.isMyGist = false;
|
||||
this.appState.localPath = filePath || null;
|
||||
this.appState.localPath = filePath;
|
||||
this.appState.templateName = templateName;
|
||||
|
||||
this.appState.setWarningDialogTexts({
|
||||
label: `Opening this fiddle will replace your unsaved changes. Do you want to proceed?`,
|
||||
ok: 'Yes'
|
||||
});
|
||||
|
||||
await window.ElectronFiddle.app.setValues(values);
|
||||
document.title = getTitle(this.appState);
|
||||
}
|
||||
@@ -83,7 +97,7 @@ export class FileManager {
|
||||
* @param {string} filePath
|
||||
* @memberof FileManager
|
||||
*/
|
||||
public async saveFiddle(filePath: string, ...transforms: Array<FileTransform>) {
|
||||
public async saveFiddle(filePath?: string, ...transforms: Array<FileTransform>) {
|
||||
const { localPath } = this.appState;
|
||||
const pathToSave = filePath || localPath;
|
||||
|
||||
@@ -95,11 +109,7 @@ export class FileManager {
|
||||
const files = await this.getFiles(undefined, ...transforms);
|
||||
|
||||
for (const [fileName, content] of files) {
|
||||
try {
|
||||
await this.saveFile(path.join(pathToSave, fileName), content);
|
||||
} catch (error) {
|
||||
console.warn(`FileManager: Failed to save file`, { fileName, error });
|
||||
}
|
||||
await this.saveFile(path.join(pathToSave, fileName), content);
|
||||
}
|
||||
|
||||
if (pathToSave !== localPath) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EditorValues } from '../interfaces';
|
||||
import { exec } from '../utils/exec';
|
||||
import { fancyImport } from '../utils/import';
|
||||
|
||||
export interface NpmOperationOptions {
|
||||
dir: string;
|
||||
@@ -8,6 +7,28 @@ export interface NpmOperationOptions {
|
||||
|
||||
export let isInstalled: boolean | null = null;
|
||||
|
||||
/* add other modules to automatically ignore here */
|
||||
/* perhaps we can expose this to the settings module?*/
|
||||
const ignoredModules: Array<string> = [
|
||||
'electron',
|
||||
// tslint:disable-next-line:no-submodule-imports
|
||||
...require('builtin-modules/static')
|
||||
];
|
||||
|
||||
/* regular expression to both match and extract module names */
|
||||
const requiregx = /require\(['"](.*?)['"]\)/gm;
|
||||
|
||||
|
||||
/*
|
||||
Quick and dirty filter functions for filtering module names
|
||||
*/
|
||||
const isIgnored = (str: string): boolean => ignoredModules.includes(str);
|
||||
const isLocalModule = (str: string): boolean => (/^[,/~\.]/.test(str));
|
||||
const isUnique = (item: any, idx: number, arr: Array<any>): boolean => {
|
||||
return arr.lastIndexOf(item) === idx;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if npm is installed by checking if a binary
|
||||
* with that name can be found.
|
||||
@@ -34,14 +55,17 @@ export async function getIsNpmInstalled(ignoreCache?: boolean): Promise<boolean>
|
||||
* @param {EditorValues} values
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export async function findModulesInEditors(values: EditorValues): Promise<Array<string>> {
|
||||
const files = [values.main, values.renderer];
|
||||
const modules: Array<string> = [];
|
||||
export function findModulesInEditors(values: EditorValues) {
|
||||
const files = [ values.main, values.renderer ];
|
||||
const modules = files.reduce(
|
||||
(agg, file) => [
|
||||
...agg,
|
||||
...findModules(file)
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
for (const file of files) {
|
||||
const fileModules = await findModules(file);
|
||||
modules.push(...fileModules);
|
||||
}
|
||||
console.log('Modules Found:', modules);
|
||||
|
||||
return modules;
|
||||
}
|
||||
@@ -49,30 +73,38 @@ export async function findModulesInEditors(values: EditorValues): Promise<Array<
|
||||
/**
|
||||
* Uses a simple regex to find `require()` statements in a string.
|
||||
* Tries to exclude electron and Node built-ins as well as file-path
|
||||
* references.
|
||||
* references. Also will try to install base packages of modules
|
||||
* that have a slash in them, for example: `lodash/fp` as the actual package
|
||||
* is just `lodash`.
|
||||
*
|
||||
* However, it WILL try to add packages that are part of a huge
|
||||
* monorepo that are named `@<group>/<package>`
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export async function findModules(input: string): Promise<Array<string>> {
|
||||
const matchRequire = /require\(['"]{1}([\w\d\/\-\_]*)['"]{1}\)/;
|
||||
const matched = input.match(matchRequire);
|
||||
const result: Array<string> = [];
|
||||
const builtinModules = (await fancyImport('builtin-modules') as any).default;
|
||||
export function findModules(input: string): Array<string> {
|
||||
/* container definitions */
|
||||
const modules: Array<string> = [];
|
||||
let match: RegExpMatchArray | null;
|
||||
|
||||
if (matched && matched.length > 0) {
|
||||
const candidates = matched.slice(1);
|
||||
candidates.forEach((candidate) => {
|
||||
if (candidate === 'electron') return;
|
||||
if (builtinModules.indexOf(candidate) > -1) return;
|
||||
if (candidate.startsWith('.')) return;
|
||||
result.push(candidate);
|
||||
});
|
||||
/* grab all global require matches in the text */
|
||||
// tslint:disable-next-line:no-conditional-assignment
|
||||
while (match = (requiregx.exec(input) || null)) {
|
||||
const mod = match[1];
|
||||
modules.push(mod);
|
||||
}
|
||||
|
||||
console.log(`findModules: Result`, result);
|
||||
|
||||
return result;
|
||||
/* map and reduce */
|
||||
return modules
|
||||
.map((mod) =>
|
||||
mod.includes('/') && !mod.startsWith('@') ?
|
||||
mod.split('/')[0] :
|
||||
mod
|
||||
)
|
||||
.filter((m) => !isIgnored(m))
|
||||
.filter((m) => !isLocalModule(m))
|
||||
.filter(isUnique);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { when } from 'mobx';
|
||||
import { EditorId, EditorValues } from '../interfaces';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
|
||||
import { getTitle } from '../utils/get-title';
|
||||
import { getOctokit } from '../utils/octokit';
|
||||
import { sortedElectronMap } from '../utils/sorted-electron-map';
|
||||
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
|
||||
import { getContent } from './content';
|
||||
import { AppState } from './state';
|
||||
import { ElectronReleaseChannel, getReleaseChannel } from './versions';
|
||||
|
||||
export class RemoteLoader {
|
||||
constructor(private readonly appState: AppState) {
|
||||
this.loadFiddleFromElectronExample.bind(this);
|
||||
this.loadFiddleFromGist.bind(this);
|
||||
this.verifyRemoteLoad.bind(this);
|
||||
this.verifyReleaseChannelEnabled.bind(this);
|
||||
this.fetchExampleAndLoad.bind(this);
|
||||
this.fetchGistAndLoad.bind(this);
|
||||
this.setElectronVersionWithRef.bind(this);
|
||||
this.getPackageVersionFromRef.bind(this);
|
||||
this.handleLoadingSuccess.bind(this);
|
||||
this.handleLoadingFailed.bind(this);
|
||||
}
|
||||
|
||||
public async loadFiddleFromElectronExample(_: any, exampleInfo: { path: string; ref: string }) {
|
||||
console.log(`Loading fiddle from Electron example`, _, exampleInfo);
|
||||
const ok = await this.verifyRemoteLoad('example from the Electron docs', exampleInfo.ref);
|
||||
if (!ok) return;
|
||||
|
||||
this.fetchExampleAndLoad(exampleInfo.ref, exampleInfo.path);
|
||||
}
|
||||
|
||||
public async loadFiddleFromGist(_: any, gistInfo: { id: string }) {
|
||||
const ok = await this.verifyRemoteLoad('gist');
|
||||
if (!ok) return;
|
||||
|
||||
this.fetchGistAndLoad(gistInfo.id);
|
||||
}
|
||||
|
||||
public async fetchExampleAndLoad(ref: string, path: string): Promise<boolean> {
|
||||
try {
|
||||
const octo = await getOctokit(this.appState);
|
||||
|
||||
const folder = await octo.repos.getContents({
|
||||
owner: ELECTRON_REPO,
|
||||
repo: ELECTRON_ORG,
|
||||
ref,
|
||||
path,
|
||||
});
|
||||
|
||||
const ok = await this.setElectronVersionWithRef(ref);
|
||||
if (!ok) return false;
|
||||
|
||||
const values = {
|
||||
html: await getContent(EditorId.html, this.appState.version),
|
||||
renderer: await getContent(EditorId.renderer, this.appState.version),
|
||||
main: await getContent(EditorId.main, this.appState.version),
|
||||
};
|
||||
|
||||
const loaders: Array<Promise<void>> = [];
|
||||
if (!Array.isArray(folder.data)) {
|
||||
throw new Error('The example Fiddle tried to launch is not a valid Electron example');
|
||||
}
|
||||
|
||||
for (const child of folder.data) {
|
||||
switch (child.name) {
|
||||
case MAIN_JS_NAME:
|
||||
loaders.push(fetch(child.download_url)
|
||||
.then((r) => r.text()).then((t) => { values.main = t; })
|
||||
);
|
||||
|
||||
break;
|
||||
case INDEX_HTML_NAME:
|
||||
loaders.push(fetch(child.download_url)
|
||||
.then((r) => r.text()).then((t) => { values.html = t; })
|
||||
);
|
||||
|
||||
break;
|
||||
case RENDERER_JS_NAME:
|
||||
loaders.push(fetch(child.download_url)
|
||||
.then((r) => r.text()).then((t) => { values.renderer = t; })
|
||||
);
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(loaders);
|
||||
|
||||
return this.handleLoadingSuccess(values, '');
|
||||
} catch (error) {
|
||||
return this.handleLoadingFailed(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a fiddle
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* @memberof AddressBar
|
||||
*/
|
||||
public async fetchGistAndLoad(gistId: string): Promise<boolean> {
|
||||
try {
|
||||
const octo = await getOctokit(this.appState);
|
||||
const gist = await octo.gists.get({ gist_id: gistId });
|
||||
|
||||
return this.handleLoadingSuccess({
|
||||
html: gist.data.files[INDEX_HTML_NAME].content,
|
||||
main: gist.data.files[MAIN_JS_NAME].content,
|
||||
renderer: gist.data.files[RENDERER_JS_NAME].content,
|
||||
}, gistId);
|
||||
} catch (error) {
|
||||
return this.handleLoadingFailed(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async setElectronVersionWithRef(ref: string): Promise<boolean> {
|
||||
const version = await this.getPackageVersionFromRef(ref);
|
||||
|
||||
const supportedVersions = sortedElectronMap(this.appState.versions, (k) => k);
|
||||
if (!supportedVersions.includes(version)) {
|
||||
this.handleLoadingFailed(new Error('Version of Electron in example not supported'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if version is part of release channel
|
||||
const versionReleaseChannel: ElectronReleaseChannel = getReleaseChannel(version);
|
||||
|
||||
if (!this.appState.versionsToShow.includes(versionReleaseChannel)) {
|
||||
const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
|
||||
if (!ok) return false;
|
||||
|
||||
this.appState.versionsToShow.push(versionReleaseChannel);
|
||||
}
|
||||
|
||||
this.appState.setVersion(version);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async getPackageVersionFromRef(ref: string): Promise<string> {
|
||||
const octo = await getOctokit(this.appState);
|
||||
const { data: packageJsonData } = await octo.repos.getContents({
|
||||
owner: ELECTRON_ORG,
|
||||
repo: ELECTRON_REPO,
|
||||
ref,
|
||||
path: 'package.json'
|
||||
});
|
||||
|
||||
const packageJsonString = Buffer.from(packageJsonData.content, 'base64').toString('utf8');
|
||||
const { version } = JSON.parse(packageJsonString);
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies from the user that we should be loading this fiddle
|
||||
*
|
||||
* @param what What are we loading from (gist, example, etc.)
|
||||
*/
|
||||
public async verifyRemoteLoad(what: string, fiddlePath?: string): Promise<boolean> {
|
||||
this.appState.setConfirmationPromptTexts({
|
||||
label: `Are you sure you sure you want to load this '${what}' from fiddle path '${fiddlePath}'? Only load and run it if you trust the source.`
|
||||
});
|
||||
this.appState.isConfirmationPromptShowing = true;
|
||||
await when(() => !this.appState.isConfirmationPromptShowing);
|
||||
|
||||
return !!this.appState.confirmationPromptLastResult;
|
||||
}
|
||||
|
||||
public async verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
|
||||
this.appState.setWarningDialogTexts({
|
||||
label: `You're loading an example with a version of Electron with an unincluded release
|
||||
channel (${channel}). Do you want to enable the release channel to load the
|
||||
version of Electron from the example?`
|
||||
});
|
||||
this.appState.isWarningDialogShowing = true;
|
||||
await when(() => !this.appState.isWarningDialogShowing);
|
||||
|
||||
return !!this.appState.warningDialogLastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading a fiddle from GitHub succeeded, let's move on.
|
||||
*
|
||||
* @param {Partial<EditorValues>} values
|
||||
* @param {string} gistId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private async handleLoadingSuccess(values: Partial<EditorValues>, gistId: string): Promise<boolean> {
|
||||
this.appState.setWarningDialogTexts({
|
||||
label: 'Loading the fiddle will replace your current unsaved changes. Do you want to discard them?'
|
||||
});
|
||||
|
||||
await window.ElectronFiddle.app.setValues(values);
|
||||
|
||||
document.title = getTitle(this.appState);
|
||||
this.appState.gistId = gistId;
|
||||
this.appState.localPath = undefined;
|
||||
this.appState.templateName = undefined;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading a fiddle from GitHub failed - this method handles this case
|
||||
* gracefully.
|
||||
*
|
||||
* @param {Error} error
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private handleLoadingFailed(error: Error): false {
|
||||
if (navigator.onLine) {
|
||||
this.appState.setWarningDialogTexts({
|
||||
label: `Loading the fiddle failed: ${error}`,
|
||||
cancel: undefined
|
||||
});
|
||||
} else {
|
||||
this.appState.setWarningDialogTexts({
|
||||
label: `Loading the fiddle failed. Your computer seems to be offline. Error: ${error}`,
|
||||
cancel: undefined
|
||||
});
|
||||
}
|
||||
|
||||
this.appState.toogleWarningDialog();
|
||||
|
||||
console.warn(`Loading Fiddle failed`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||