Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a223fd3dc | ||
|
|
70d4902b87 | ||
|
|
a93a99e3f0 | ||
|
|
4c209159e6 | ||
|
|
b09a248c96 | ||
|
|
6f09ed9c2a | ||
|
|
c1ae0ecd86 | ||
|
|
1e9a81fcf4 | ||
|
|
2061b26ed6 | ||
|
|
88ce4a76e9 | ||
|
|
9b4ef4c0d2 | ||
|
|
dca2cd67e5 | ||
|
|
6bab353088 | ||
|
|
e6cc44ca37 | ||
|
|
d039c5dbf5 | ||
|
|
c155fff8b0 | ||
|
|
11202a8d6b | ||
|
|
475127f5b1 | ||
|
|
feee5be2f8 | ||
|
|
678c7f1578 | ||
|
|
d2fcb8071b | ||
|
|
0ab35dc867 | ||
|
|
377d221191 | ||
|
|
cbdab9d662 | ||
|
|
6875ab392e | ||
|
|
58ec0f015c | ||
|
|
2aef0e776e | ||
|
|
b2a7d6b2a4 | ||
|
|
303d6bc79a | ||
|
|
d028e7c915 | ||
|
|
f04631b6c1 | ||
|
|
c1bb524b59 | ||
|
|
4a38fda293 | ||
|
|
b98ad2b572 | ||
|
|
6118b94b09 | ||
|
|
d0c1ee1a82 | ||
|
|
525715a911 | ||
|
|
afbc0c9e6c | ||
|
|
c1a9cdf88b | ||
|
|
d281e61b8a | ||
|
|
2fbbc10497 | ||
|
|
e8226ca97d | ||
|
|
08f2dbe937 | ||
|
|
68813edb51 | ||
|
|
a32261414b | ||
|
|
42f67e7b21 | ||
|
|
7d9b0ed79d | ||
|
|
47f1b5c4c3 | ||
|
|
432322694d | ||
|
|
889e76cbae | ||
|
|
4a689e5c06 | ||
|
|
3113f324cc | ||
|
|
64addb1fb0 | ||
|
|
e6facf60c1 | ||
|
|
00cb5cf6c6 | ||
|
|
3e8dbebe42 | ||
|
|
dcdef4aa4e | ||
|
|
590389abdc | ||
|
|
18a8f0c142 | ||
|
|
a8e8ab6f04 | ||
|
|
45ed35467a | ||
|
|
eb49cb296a | ||
|
|
c2344d50d2 | ||
|
|
7786e113d9 | ||
|
|
f8b51870d8 | ||
|
|
601c28dcf8 | ||
|
|
49379af856 | ||
|
|
8347f79756 | ||
|
|
c102128d5a | ||
|
|
fcff053136 | ||
|
|
c3641cfa04 | ||
|
|
6eba3cbdcc | ||
|
|
6393c59d5c | ||
|
|
543e86bc91 | ||
|
|
ea5fdd426c | ||
|
|
fecc499cc5 | ||
|
|
8d0a1524b9 | ||
|
|
918e91965a | ||
|
|
15bab9ff86 | ||
|
|
7d879b59cf | ||
|
|
72f7e058d6 | ||
|
|
a65e044b5c | ||
|
|
a4c4590816 | ||
|
|
6fbb816be3 | ||
|
|
ae0ba026f2 | ||
|
|
cc2a44d10a | ||
|
|
3e6eb16974 | ||
|
|
4212284fc4 | ||
|
|
8b35ff8f95 | ||
|
|
4d00ee9fd4 | ||
|
|
590db5304d | ||
|
|
e519ce2f13 | ||
|
|
8221920fab | ||
|
|
5de7ed7d9d | ||
|
|
a1586e864a | ||
|
|
60745ff8bb | ||
|
|
4ac20aa5c2 | ||
|
|
48faec9741 | ||
|
|
c0f4e95a67 | ||
|
|
d5c8c8bd96 | ||
|
|
80ff396879 | ||
|
|
bf42abc88d | ||
|
|
eaa064ea8d | ||
|
|
e804436669 | ||
|
|
08d07b34a4 | ||
|
|
74ba7db13c | ||
|
|
0fced3aefd | ||
|
|
13c9217f9f | ||
|
|
346742b242 | ||
|
|
aefe273516 | ||
|
|
337d19c33a | ||
|
|
ebc6f91dea | ||
|
|
25446aa9e4 | ||
|
|
f2859c053b | ||
|
|
49cd192817 | ||
|
|
9bdd09c6b5 | ||
|
|
c3d36b29b0 | ||
|
|
1b69a2ff52 | ||
|
|
311497d080 | ||
|
|
55b7739d36 | ||
|
|
0fda97eb2f | ||
|
|
879815fc02 | ||
|
|
c469c3e856 | ||
|
|
1b6750e0c5 | ||
|
|
f14bd5c041 | ||
|
|
76115a3e9c | ||
|
|
65f86e0177 | ||
|
|
18cacb1532 | ||
|
|
6a924ac88b | ||
|
|
d125af6775 | ||
|
|
1a9a0579a5 | ||
|
|
acee131288 | ||
|
|
66536a5260 | ||
|
|
7941a83109 | ||
|
|
b312b80b69 | ||
|
|
b3c3b73b7d | ||
|
|
0a8e9cde1b | ||
|
|
ca3f48697d | ||
|
|
067e5cd5e2 | ||
|
|
7e16c8c1f2 | ||
|
|
e932aad47b | ||
|
|
82fb8290c3 | ||
|
|
2eb8bb3cf5 | ||
|
|
3d6448030a | ||
|
|
65855908d1 | ||
|
|
9e986e24e2 | ||
|
|
9abc358bd9 | ||
|
|
7f7785c69c | ||
|
|
f36f300feb | ||
|
|
2c49214a25 | ||
|
|
c7a8163990 | ||
|
|
97995b0ec2 | ||
|
|
d2377dcc30 | ||
|
|
ac1197faaa | ||
|
|
9fb9aa6c92 | ||
|
|
6c5e41a2f1 | ||
|
|
4b52c81101 | ||
|
|
35f4f1293c | ||
|
|
8840b1ef7f | ||
|
|
d516522af2 | ||
|
|
0d0a51a900 | ||
|
|
be3b1e4a5b | ||
|
|
2acc7112b7 | ||
|
|
407bf3caeb | ||
|
|
49123116d5 | ||
|
|
9f6bc2f26f | ||
|
|
5585e2d5d0 | ||
|
|
4235969526 | ||
|
|
659d41f1c8 | ||
|
|
2549992782 | ||
|
|
b7314b83ee | ||
|
|
39c2a9164a | ||
|
|
1a9dc1fbf7 | ||
|
|
0eb01c4692 | ||
|
|
96667a3b04 | ||
|
|
4365513f57 | ||
|
|
913b37c285 | ||
|
|
66c55d39c0 | ||
|
|
0f3a3aca72 | ||
|
|
3fc974a758 | ||
|
|
72b805059c | ||
|
|
291882ac5c | ||
|
|
3c05cc8ec7 | ||
|
|
75d2f9ff72 | ||
|
|
0119f0ce69 | ||
|
|
8ed5ea94b5 | ||
|
|
deaac4e15c | ||
|
|
48e58c095d | ||
|
|
28f0eca429 | ||
|
|
529906e1a9 | ||
|
|
40c9aaeb7a | ||
|
|
15924c7741 | ||
|
|
c62041a000 | ||
|
|
ff3e931d14 | ||
|
|
7cc16b6fab | ||
|
|
45e8e311da | ||
|
|
b09e792583 | ||
|
|
cdb862442c | ||
|
|
1e1566dbdb | ||
|
|
e571fcca4f | ||
|
|
e61e49987e | ||
|
|
133411c150 | ||
|
|
db8c7c32ab | ||
|
|
003e27f57e | ||
|
|
72d717bd48 | ||
|
|
675500fe25 | ||
|
|
fbe2311d53 | ||
|
|
ef42b1affe | ||
|
|
2adde7d9ce | ||
|
|
5fa0ddcc73 | ||
|
|
5d501f0827 | ||
|
|
d738936952 | ||
|
|
57ce8b0b79 | ||
|
|
46f4efb4b4 | ||
|
|
10a696a9d7 | ||
|
|
585c0e1183 | ||
|
|
946d32fdd0 | ||
|
|
f09363ec26 | ||
|
|
1a6876497a | ||
|
|
6bc04f5d2a | ||
|
|
9f7bf9d299 | ||
|
|
3b7873917f | ||
|
|
4616f12d6c | ||
|
|
63f37c9553 | ||
|
|
09edfa16b0 | ||
|
|
da3a607bc1 | ||
|
|
5fcc34b035 | ||
|
|
dfed297226 | ||
|
|
7cb2c4d188 | ||
|
|
f0215d8c90 | ||
|
|
ffdb57e2f3 | ||
|
|
e5dd6ffb4e | ||
|
|
1083b6c2de | ||
|
|
73b6cb0cb2 | ||
|
|
3e5914b192 | ||
|
|
fe81d5c14c | ||
|
|
c85536a8b9 | ||
|
|
8d90dbed09 | ||
|
|
b918c13c72 | ||
|
|
570bab9c29 | ||
|
|
f0f7ed22ee | ||
|
|
a62c1550a1 | ||
|
|
754473e10a | ||
|
|
732f81cd33 | ||
|
|
47e5182117 | ||
|
|
92c831cc7b | ||
|
|
8ffb215797 | ||
|
|
616264f017 | ||
|
|
ee6e474b04 | ||
|
|
ee8fa678f9 |
@@ -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,6 +1,6 @@
|
||||
# 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,
|
||||
@@ -15,7 +15,7 @@ address bar.
|
||||
</a>
|
||||
</h3>
|
||||
|
||||

|
||||
<img src="https://user-images.githubusercontent.com/378023/49716908-7d2b0500-fc98-11e8-8f52-def504802509.png" width="880px" alt="Electron Fiddle screenshot">
|
||||
|
||||
# Features
|
||||
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
draft: true,
|
||||
prerelease: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
],
|
||||
"coverageReporters": [
|
||||
"json",
|
||||
"html"
|
||||
"html",
|
||||
"lcov"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"!**/*.d.ts"
|
||||
"!**/*.d.ts",
|
||||
"!**/*constants.ts"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "electron-fiddle",
|
||||
"productName": "Electron Fiddle",
|
||||
"version": "0.2.0",
|
||||
"version": "0.5.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",
|
||||
@@ -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,70 +36,71 @@
|
||||
"forge": "./forge.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.13",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.0-14",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.1.0-11",
|
||||
"@fortawesome/react-fontawesome": "0.1.0-11",
|
||||
"@octokit/rest": "^15.9.4",
|
||||
"@blueprintjs/core": "^3.10.0",
|
||||
"@blueprintjs/select": "^3.4.0",
|
||||
"@octokit/rest": "^15.12.1",
|
||||
"builtin-modules": "^3.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"electron-default-menu": "^1.0.1",
|
||||
"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.1.0",
|
||||
"mobx-react": "^5.2.5",
|
||||
"monaco-loader": "^0.14.0",
|
||||
"namor": "^1.0.1",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2",
|
||||
"react-mosaic-component": "^1.1.1",
|
||||
"semver": "^5.5.1",
|
||||
"fs-extra": "^7.0.1",
|
||||
"mobx": "^5.8.0",
|
||||
"mobx-react": "^5.4.3",
|
||||
"monaco-loader": "^0.15.0",
|
||||
"namor": "^1.1.1",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-mosaic-component": "^2.0.2",
|
||||
"semver": "^5.6.0",
|
||||
"tmp": "0.0.33",
|
||||
"tslib": "^1.9.3",
|
||||
"update-electron-app": "^1.3.0"
|
||||
},
|
||||
"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",
|
||||
"@electron-forge/cli": "^6.0.0-beta.32",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.32",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.32",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.32",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.32",
|
||||
"@electron-forge/publisher-github": "^6.0.0-beta.32",
|
||||
"@types/builtin-modules": "^2.0.0",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/enzyme": "^3.1.13",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/jest": "^23.3.1",
|
||||
"@types/jest": "^23.3.2",
|
||||
"@types/log-symbols": "^2.0.0",
|
||||
"@types/node": "^10.5.1",
|
||||
"@types/react": "^16.4.12",
|
||||
"@types/react-dom": "^16.0.7",
|
||||
"@types/node": "^10.11.1",
|
||||
"@types/react": "^16.7.18",
|
||||
"@types/react-dom": "^16.0.11",
|
||||
"@types/semver": "^5.5.0",
|
||||
"@types/tmp": "0.0.33",
|
||||
"chokidar": "^2.0.4",
|
||||
"electron": "2.0.8",
|
||||
"enzyme": "^3.5.0",
|
||||
"enzyme-adapter-react-16": "^1.3.0",
|
||||
"enzyme-to-json": "^3.3.4",
|
||||
"jest": "^23.5.0",
|
||||
"jest-fetch-mock": "^1.6.5",
|
||||
"coveralls": "^3.0.2",
|
||||
"electron": "4.0.2",
|
||||
"enzyme": "^3.6.0",
|
||||
"enzyme-adapter-react-16": "^1.5.0",
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"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-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",
|
||||
"standard": "^11.0.1",
|
||||
"stylelint": "^9.3.0",
|
||||
"monaco-editor": "^0.15.6",
|
||||
"node-abi": "^2.5.1",
|
||||
"node-fetch": "^2.3.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"parcel-bundler": "^1.11.0",
|
||||
"react-test-renderer": "^16.7.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"standard": "^12.0.1",
|
||||
"stylelint": "^9.9.0",
|
||||
"stylelint-config-standard": "^18.2.0",
|
||||
"ts-jest": "^23.1.4",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-microsoft-contrib": "^5.2.1",
|
||||
"ts-jest": "^23.10.5",
|
||||
"tslint": "^5.12.0",
|
||||
"tslint-microsoft-contrib": "^6.0.0",
|
||||
"tslint-react": "^3.6.0",
|
||||
"typescript": "^3.0.1"
|
||||
"typescript": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
declare module 'fix-path';
|
||||
declare module 'namor';
|
||||
@@ -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')
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
export type ElectronVersionState = 'downloading' | 'ready' | 'unknown';
|
||||
|
||||
export type WindowNames = 'main';
|
||||
|
||||
export type Files = Map<string, string>;
|
||||
|
||||
export type FileTransform = (files: Files) => Promise<Files>;
|
||||
|
||||
export interface GitHubVersion {
|
||||
url: string;
|
||||
assets_url: string;
|
||||
html_url: string;
|
||||
tag_name: string;
|
||||
target_commitish: string;
|
||||
name: string;
|
||||
prerelease: boolean;
|
||||
created_at: string;
|
||||
published_at: string;
|
||||
body: string;
|
||||
export enum ElectronVersionState {
|
||||
ready = 'ready',
|
||||
downloading = 'downloading',
|
||||
unknown = 'unknown'
|
||||
}
|
||||
|
||||
export enum ElectronVersionSource {
|
||||
remote = 'remote',
|
||||
local = 'local'
|
||||
}
|
||||
export interface NpmVersion {
|
||||
version: string;
|
||||
name?: string;
|
||||
localPath?: string;
|
||||
}
|
||||
|
||||
export interface EditorValues {
|
||||
@@ -26,8 +27,9 @@ export interface EditorValues {
|
||||
package?: string;
|
||||
}
|
||||
|
||||
export interface ElectronVersion extends GitHubVersion {
|
||||
export interface ElectronVersion extends NpmVersion {
|
||||
state: ElectronVersionState;
|
||||
source: ElectronVersionSource;
|
||||
}
|
||||
|
||||
export interface OutputEntry {
|
||||
@@ -41,6 +43,11 @@ export interface OutputOptions {
|
||||
isNotPre?: boolean;
|
||||
}
|
||||
|
||||
export interface WarningDialogTexts {
|
||||
ok?: string;
|
||||
cancel?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Templates {
|
||||
[index: string]: string | Templates;
|
||||
|
||||
@@ -5,6 +5,7 @@ export enum IpcEvents {
|
||||
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,9 +16,7 @@ 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_WELCOME_TOUR = 'SHOW_WELCOME_TOUR',
|
||||
TOGGLE_SOFT_WRAP = 'TOGGLE_SOFT_WRAP',
|
||||
TOGGLE_MINI_MAP = 'TOGGLE_MINI_MAP'
|
||||
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR'
|
||||
}
|
||||
|
||||
export const ipcMainEvents = [
|
||||
@@ -33,6 +32,7 @@ export const ipcRendererEvents = [
|
||||
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,
|
||||
@@ -40,7 +40,5 @@ export const ipcRendererEvents = [
|
||||
IpcEvents.FS_SAVE_FIDDLE_AS,
|
||||
IpcEvents.FS_SAVE_FIDDLE_FORGE,
|
||||
IpcEvents.FS_SAVE_FIDDLE_GIST,
|
||||
IpcEvents.SHOW_WELCOME_TOUR,
|
||||
IpcEvents.TOGGLE_SOFT_WRAP,
|
||||
IpcEvents.TOGGLE_MINI_MAP
|
||||
IpcEvents.SHOW_WELCOME_TOUR
|
||||
];
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
@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-menu-item.bp3-active.bp3-intent-primary {
|
||||
background-color: @foreground-3;
|
||||
}
|
||||
|
||||
.bp3-dialog {
|
||||
background-color: @background-4;
|
||||
|
||||
.bp3-dialog-header {
|
||||
background-color: @background-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,66 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
& > 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 {
|
||||
@@ -161,7 +157,6 @@
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
color: rgba(159, 234, 249, 0.5);
|
||||
height: 28px;
|
||||
margin-top: 10px;
|
||||
@@ -172,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +183,16 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dialog-add-version {
|
||||
.bp3-file-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bp3-callout {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
@@ -1,8 +1,8 @@
|
||||
@import "variables.less";
|
||||
@import "../variables.less";
|
||||
|
||||
.fiddle {
|
||||
.mosaic-window-toolbar.draggable {
|
||||
background: var(--background-2);
|
||||
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);
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
height: 100vh;
|
||||
|
||||
&.mosaic-blueprint-theme {
|
||||
background: @background-2;
|
||||
background: #bdbdbd15;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "variables.less";
|
||||
@import "../variables.less";
|
||||
|
||||
.output {
|
||||
font-size: 12px;
|
||||
@@ -12,8 +12,7 @@
|
||||
padding-left: 10px;
|
||||
height: 0;
|
||||
overflow-y: scroll;
|
||||
transition: height 0.2s;
|
||||
transition: padding 0.2s;
|
||||
transition: height 0.2s linear, padding 0.2s linear;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&.showing {
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "variables.less";
|
||||
@import "../variables.less";
|
||||
|
||||
.settings {
|
||||
background: @background-3;
|
||||
@@ -35,6 +35,25 @@
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.bp3-divider {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.bp3-callout {
|
||||
max-width: 850px;
|
||||
}
|
||||
|
||||
.electron-versions-table {
|
||||
width: 100%;
|
||||
|
||||
.action {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@@ -45,8 +64,27 @@
|
||||
}
|
||||
|
||||
.settings-electron {
|
||||
.advanced-options-buttons {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
> div {
|
||||
margin-right: 1rem;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> div:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.versions-to-show {
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid @border-color-1;
|
||||
padding-bottom: 1.5rem;
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
@@ -59,18 +97,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.electron-versions-table {
|
||||
button {
|
||||
margin: 0;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.action {
|
||||
text-align: center;
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
@@ -148,25 +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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-appearance {
|
||||
button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,7 @@
|
||||
width: 100vw;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.tour-portal {
|
||||
z-index: 1000;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
button,
|
||||
input[type='button'],
|
||||
input[type='reset'],
|
||||
input[type='submit'] {
|
||||
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;
|
||||
|
||||
&: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,58 +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: 100%;
|
||||
|
||||
&: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,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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2,25 +2,19 @@ import { app } from 'electron';
|
||||
|
||||
import { isDevMode } from '../utils/devmode';
|
||||
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();
|
||||
|
||||
@@ -32,15 +26,44 @@ app.on('ready', async () => {
|
||||
setupFileListeners();
|
||||
setupUpdates();
|
||||
setupDialogs();
|
||||
});
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
/**
|
||||
* 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('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)
|
||||
},
|
||||
{
|
||||
@@ -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' ]),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ 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;
|
||||
|
||||
@@ -30,8 +32,8 @@ export const scanArgv = (argv: Array<string>) => {
|
||||
};
|
||||
|
||||
export const listenForProtocolHandler = () => {
|
||||
const shouldQuit = app.makeSingleInstance(scanArgv);
|
||||
if (shouldQuit) return app.quit();
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) app.quit();
|
||||
|
||||
app.on('open-url', (_, url) => {
|
||||
if (url.startsWith(`${PROTOCOL}://`)) {
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -33,10 +33,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,9 +1,11 @@
|
||||
import { when } from 'mobx';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
|
||||
import { EditorValues } from '../interfaces';
|
||||
import { updateEditorLayout } from '../utils/editor-layout';
|
||||
import { getPackageJson, PackageJsonOptions } from '../utils/get-package';
|
||||
import { FileManager } from './file-manager';
|
||||
import { Runner } from './runner';
|
||||
import { appState } from './state';
|
||||
import { getTheme } from './themes';
|
||||
|
||||
@@ -18,6 +20,7 @@ export class App {
|
||||
public monaco: typeof MonacoType | null = null;
|
||||
public state = appState;
|
||||
public fileManager = new FileManager(appState);
|
||||
public runner = new Runner(appState);
|
||||
|
||||
constructor() {
|
||||
this.getValues = this.getValues.bind(this);
|
||||
@@ -28,17 +31,22 @@ export class App {
|
||||
* 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;
|
||||
@@ -55,7 +63,7 @@ export class App {
|
||||
renderer.setValue(values.renderer);
|
||||
}
|
||||
|
||||
appState.isUnsaved = false;
|
||||
this.state.isUnsaved = false;
|
||||
this.setupUnsavedOnChangeListener();
|
||||
|
||||
return true;
|
||||
@@ -81,7 +89,7 @@ export class App {
|
||||
};
|
||||
|
||||
if (options && options.include !== false) {
|
||||
values.package = await getPackageJson(appState, values, options);
|
||||
values.package = await getPackageJson(this.state, values, options);
|
||||
}
|
||||
|
||||
return values;
|
||||
@@ -91,7 +99,7 @@ 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');
|
||||
@@ -103,13 +111,13 @@ export class App {
|
||||
const className = `${process.platform} container`;
|
||||
const app = (
|
||||
<div className={className}>
|
||||
<Header appState={appState} />
|
||||
<Dialogs appState={appState} />
|
||||
<Editors appState={appState} />
|
||||
<Header appState={this.state} />
|
||||
<Dialogs appState={this.state} />
|
||||
<Editors appState={this.state} />
|
||||
</div>
|
||||
);
|
||||
|
||||
render(app, document.getElementById('app'));
|
||||
const rendered = render(app, document.getElementById('app'));
|
||||
|
||||
this.setupResizeListener();
|
||||
|
||||
@@ -118,6 +126,8 @@ export class App {
|
||||
setTimeout(() => {
|
||||
this.setupUnsavedOnChangeListener();
|
||||
}, 1500);
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +139,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 +152,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 +174,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,19 @@ export class BinaryManager {
|
||||
|
||||
const zipPath = await eDownload({ version });
|
||||
const extractPath = this.getDownloadPath(version);
|
||||
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking`);
|
||||
const electronFiles = await this.unzip(zipPath, extractPath);
|
||||
console.log(electronFiles);
|
||||
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking to ${extractPath}`);
|
||||
|
||||
try {
|
||||
// Ensure the target path is empty
|
||||
await fs.emptyDir(extractPath);
|
||||
|
||||
const electronFiles = await this.unzip(zipPath, extractPath);
|
||||
console.log(`Unzipped ${version}`, electronFiles);
|
||||
} catch (error) {
|
||||
console.warn(`Failure while unzipping ${version}`, error);
|
||||
|
||||
// Todo: Handle this case
|
||||
}
|
||||
|
||||
this.state[version] = 'ready';
|
||||
}
|
||||
@@ -89,11 +99,13 @@ export class BinaryManager {
|
||||
* Gets the expected path for the binary of a given Electron version
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} dir
|
||||
* @returns {string}
|
||||
*/
|
||||
public getElectronBinaryPath(version: string): string {
|
||||
const dir = this.getDownloadPath(version);
|
||||
|
||||
public getElectronBinaryPath(
|
||||
version: string,
|
||||
dir: string = this.getDownloadPath(version),
|
||||
): string {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return path.join(dir, 'Electron.app/Contents/MacOS/Electron');
|
||||
@@ -138,11 +150,13 @@ export class BinaryManager {
|
||||
* Did we already download a given version?
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} dir
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public async getIsDownloaded(version: string): Promise<boolean> {
|
||||
const expectedPath = this.getElectronBinaryPath(version);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -169,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,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>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Button, InputGroup, Intent } from '@blueprintjs/core';
|
||||
import classnames from 'classnames';
|
||||
import { reaction } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../constants';
|
||||
import { classNames } from '../../utils/classnames';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../shared-constants';
|
||||
import { getTitle } from '../../utils/get-title';
|
||||
import { idFromUrl } from '../../utils/gist';
|
||||
import { idFromUrl, urlFromId } from '../../utils/gist';
|
||||
import { getOctokit } from '../../utils/octokit';
|
||||
import { AppState } from '../state';
|
||||
|
||||
@@ -26,9 +27,10 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
value: this.props.appState.gistId
|
||||
};
|
||||
const { gistId } = this.props.appState;
|
||||
const value = gistId ? urlFromId(gistId) : '';
|
||||
|
||||
this.state = { value };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +57,7 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
|
||||
public componentDidMount() {
|
||||
reaction(
|
||||
() => this.props.appState.gistId,
|
||||
(gistId: string) => this.setState({ value: gistId })
|
||||
(gistId: string) => this.setState({ value: urlFromId(gistId) })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,15 +70,6 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
|
||||
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
|
||||
*
|
||||
@@ -86,15 +79,14 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
|
||||
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,
|
||||
id: appState.gistId,
|
||||
gist_id: appState.gistId
|
||||
});
|
||||
|
||||
this.props.appState.setWarningDialogTexts({
|
||||
label: 'Loading the fiddle will replace your current unsaved changes. Do you want to discard them?'
|
||||
});
|
||||
|
||||
await window.ElectronFiddle.app.setValues({
|
||||
@@ -113,21 +105,33 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
|
||||
return true;
|
||||
}
|
||||
|
||||
public renderLoadButton(isValueCorrect: boolean): JSX.Element {
|
||||
return (
|
||||
<Button
|
||||
disabled={!isValueCorrect}
|
||||
icon='cloud-download'
|
||||
text='Load Fiddle'
|
||||
onClick={this.loadFiddle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 });
|
||||
const isCorrect = /https:\/\/gist\.github\.com\/(.+)$/.test(value);
|
||||
const className = classnames('address-bar', isUnsaved, { empty: !value });
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
<InputGroup
|
||||
key='addressbar'
|
||||
pattern='https:\/\/gist\.github\.com\/(.+)$'
|
||||
leftIcon='geosearch'
|
||||
intent={isCorrect || !value ? undefined : Intent.DANGER}
|
||||
onChange={this.handleChange}
|
||||
placeholder='https://gist.github.com/...'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onInvalid={this.handleInvalid}
|
||||
rightElement={this.renderLoadButton(isCorrect)}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
@@ -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';
|
||||
@@ -34,6 +32,8 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
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() {
|
||||
@@ -79,12 +79,13 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
token: this.props.appState.gitHubToken!
|
||||
});
|
||||
|
||||
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]: {
|
||||
@@ -97,7 +98,7 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
content: values.renderer || '// Empty',
|
||||
},
|
||||
},
|
||||
});
|
||||
} as any); // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
|
||||
|
||||
this.props.appState.gistId = gist.data.id;
|
||||
|
||||
@@ -116,18 +117,69 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
|
||||
this.setState({ isPublishing: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish fiddles as private.
|
||||
*
|
||||
* @memberof PublishButton
|
||||
*/
|
||||
public setPrivate() {
|
||||
this.setPrivacy(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish fiddles as public.
|
||||
*
|
||||
* @memberof PublishButton
|
||||
*/
|
||||
public setPublic() {
|
||||
this.setPrivacy(true);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { gitHubPublishAsPublic } = this.props.appState;
|
||||
const { isPublishing } = this.state;
|
||||
const className = classNames('button', 'button-publish', isPublishing);
|
||||
const icon = isPublishing ? faSpinner : faUpload;
|
||||
const text = isPublishing ? 'Publishing...' : 'Publish';
|
||||
|
||||
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>
|
||||
<ButtonGroup className='button-publish'>
|
||||
<Popover
|
||||
content={privacyMenu}
|
||||
position={Position.BOTTOM}
|
||||
disabled={isPublishing}
|
||||
>
|
||||
<Button
|
||||
icon={privacyIcon}
|
||||
/>
|
||||
</Popover>
|
||||
<Button
|
||||
onClick={this.handleClick}
|
||||
loading={isPublishing}
|
||||
disabled={isPublishing}
|
||||
icon='upload'
|
||||
text={isPublishing ? 'Publishing...' : 'Publish'}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
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} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
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 } = this.props.appState;
|
||||
|
||||
return sortedElectronMap<ElectronVersion>(versions, (_key, item) => item)
|
||||
.filter((item) => !!item && versionsToShow.includes(getReleaseChannel(item)));
|
||||
}
|
||||
|
||||
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,12 @@
|
||||
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 { PublishButton } from './commands-publish-button';
|
||||
import { Runner } from './commands-runner';
|
||||
import { VersionChooser } from './commands-version-chooser';
|
||||
|
||||
export interface CommandsProps {
|
||||
appState: AppState;
|
||||
@@ -32,15 +31,22 @@ 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}
|
||||
/>
|
||||
</ControlGroup>
|
||||
</div>
|
||||
<div>
|
||||
<AddressBar appState={appState} />
|
||||
<PublishButton appState={appState} />
|
||||
<button className='button' onClick={() => appState.toggleConsole()}>
|
||||
<FontAwesomeIcon icon={faTerminal} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
|
||||
import { Button, Callout, Dialog, FileInput, InputGroup, Intent } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { NpmVersion } from '../../interfaces';
|
||||
import { getElectronNameForPlatform } from '../../utils/electron-name';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface AddVersionDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface AddVersionDialogState {
|
||||
isValidElectron: boolean;
|
||||
isValidVersion: boolean;
|
||||
file?: File;
|
||||
version: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "add version" dialog allows users to add custom builds of Electron.
|
||||
*
|
||||
* @class AddVersionDialog
|
||||
* @extends {React.Component<AddVersionDialogProps, AddVersionDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class AddVersionDialog extends React.Component<AddVersionDialogProps, AddVersionDialogState> {
|
||||
constructor(props: AddVersionDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isValidVersion: false,
|
||||
isValidElectron: false,
|
||||
version: ''
|
||||
};
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onChangeFile = this.onChangeFile.bind(this);
|
||||
this.onChangeVersion = this.onChangeVersion.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change of the file input
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
|
||||
const { files } = event.target as any;
|
||||
const { binaryManager } = this.props.appState;
|
||||
const file = files && files[0] ? files[0] : undefined;
|
||||
|
||||
const isValidElectron = !!(file
|
||||
&& file.path
|
||||
&& await binaryManager.getIsDownloaded('custom', file.path));
|
||||
|
||||
this.setState({ file, isValidElectron });
|
||||
}
|
||||
|
||||
public onChangeVersion(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const version = event.target.value || '';
|
||||
const isValidVersion = !!semver.valid(version);
|
||||
|
||||
this.setState({
|
||||
version,
|
||||
isValidVersion,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of the dialog
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async onSubmit(): Promise<void> {
|
||||
const { file, version } = this.state;
|
||||
|
||||
if (!file) return;
|
||||
|
||||
const name = file.path
|
||||
.slice(-20)
|
||||
.split(path.sep)
|
||||
.slice(1)
|
||||
.join(path.sep);
|
||||
|
||||
const toAdd: NpmVersion = {
|
||||
localPath: file.path,
|
||||
version,
|
||||
name
|
||||
};
|
||||
|
||||
this.props.appState.addLocalVersion(toAdd);
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog
|
||||
*/
|
||||
public onClose() {
|
||||
this.props.appState.isAddVersionDialogShowing = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
const canSubmit = this.state.isValidElectron && this.state.isValidVersion;
|
||||
|
||||
return [
|
||||
(
|
||||
<Button
|
||||
icon='add'
|
||||
key='submit'
|
||||
disabled={!canSubmit}
|
||||
onClick={this.onSubmit}
|
||||
text='Add'
|
||||
/>
|
||||
), (
|
||||
<Button
|
||||
icon='cross'
|
||||
key='cancel'
|
||||
onClick={this.onClose}
|
||||
text='Cancel'
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isAddVersionDialogShowing } = this.props.appState;
|
||||
const inputProps = { webkitdirectory: 'true' };
|
||||
const { file } = this.state;
|
||||
|
||||
const text = file && file.path
|
||||
? file.path
|
||||
: `Select the folder containing ${getElectronNameForPlatform()}...`;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isAddVersionDialogShowing}
|
||||
onClose={this.onClose}
|
||||
title='Add local Electron build'
|
||||
className='dialog-add-version'
|
||||
>
|
||||
<div className='bp3-dialog-body'>
|
||||
<FileInput
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPath(): JSX.Element | null {
|
||||
const { file, isValidElectron } = this.state;
|
||||
|
||||
if (!file || !file.path) return null;
|
||||
|
||||
const info = isValidElectron
|
||||
? `We found an ${getElectronNameForPlatform()} in this folder.`
|
||||
: `We did not find a ${getElectronNameForPlatform()} in this folder...`;
|
||||
|
||||
return (
|
||||
<Callout>
|
||||
{info}
|
||||
{this.renderVersionInput()}
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
private renderVersionInput(): JSX.Element | null {
|
||||
const { isValidElectron, isValidVersion, version } = this.state;
|
||||
|
||||
if (!isValidElectron) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
Please specify a version, used for typings and the name.
|
||||
Must be <code>semver</code> compliant.
|
||||
</p>
|
||||
<InputGroup
|
||||
intent={isValidVersion ? undefined : Intent.DANGER}
|
||||
value={version}
|
||||
onChange={this.onChangeVersion}
|
||||
placeholder='4.0.0'
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this component's state
|
||||
*/
|
||||
private reset(): void {
|
||||
this.setState({
|
||||
isValidElectron: false,
|
||||
isValidVersion: false,
|
||||
version: '',
|
||||
file: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,14 +55,12 @@ 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 });
|
||||
|
||||
const octo = await getOctokit();
|
||||
octo.authenticate({
|
||||
type: 'token',
|
||||
token: this.state.tokenInput || '',
|
||||
token: this.state.tokenInput,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -138,64 +134,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,52 @@
|
||||
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface WarningDialogProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export interface WarningDialogState {
|
||||
}
|
||||
|
||||
/**
|
||||
* The token dialog asks the user for a GitHub Personal Access Token.
|
||||
* It's also responsible for checking if the token is correct.
|
||||
*
|
||||
* @export
|
||||
* @class WarningDialog
|
||||
* @extends {React.Component<WarningDialogProps, WarningDialogState>}
|
||||
*/
|
||||
@observer
|
||||
export class WarningDialog extends React.Component<WarningDialogProps, WarningDialogState> {
|
||||
constructor(props: WarningDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
public onClose(result: boolean) {
|
||||
this.props.appState.warningDialogLastResult = result;
|
||||
this.props.appState.toogleWarningDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isWarningDialogShowing, warningDialogTexts } = this.props.appState;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
isOpen={isWarningDialogShowing}
|
||||
onClose={this.onClose}
|
||||
icon='warning-sign'
|
||||
confirmButtonText={warningDialogTexts.ok}
|
||||
cancelButtonText={warningDialogTexts.cancel}
|
||||
intent={Intent.DANGER}
|
||||
>
|
||||
<p>{warningDialogTexts.label}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ArrowPosition } from '../../interfaces';
|
||||
import { classNames } from '../../utils/classnames';
|
||||
import { sortButtons } from '../../utils/sort-buttons';
|
||||
|
||||
export interface DialogProps {
|
||||
buttons?: Array<JSX.Element> | null;
|
||||
style?: React.CSSProperties;
|
||||
isCentered?: boolean;
|
||||
className?: string;
|
||||
isShowing?: boolean;
|
||||
isShowingBackdrop?: boolean;
|
||||
arrow?: ArrowPosition;
|
||||
onClose?: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic dialog class for Fiddle.
|
||||
*
|
||||
* @class Dialog
|
||||
* @extends {React.Component<DialogProps, {}>}
|
||||
*/
|
||||
export class Dialog extends React.Component<DialogProps, {}> {
|
||||
public static defaultProps: Partial<DialogProps> = {
|
||||
isCentered: false,
|
||||
isShowing: true,
|
||||
isShowingBackdrop: false
|
||||
};
|
||||
|
||||
constructor(props: DialogProps) {
|
||||
super(props);
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onConfirm = this.onConfirm.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog
|
||||
*/
|
||||
public onClose() {
|
||||
if (this.props.onClose) this.props.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the dialog
|
||||
*/
|
||||
public onConfirm() {
|
||||
if (this.props.onConfirm) this.props.onConfirm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the dialog's buttons, using a default "Ok"
|
||||
* if none were passed.
|
||||
*
|
||||
* @returns {Array<JSX.Element> | JSX.Element}
|
||||
*/
|
||||
public renderButtons(): Array<JSX.Element | null> | JSX.Element {
|
||||
const { buttons, onClose } = this.props;
|
||||
const closeButton = onClose
|
||||
? <button key='cancel' className='btn-close' onClick={this.onClose}>Cancel</button>
|
||||
: null;
|
||||
const okButton = (
|
||||
<button key='ok' className='btn-ok' onClick={this.onConfirm}>Ok</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='dialog-buttons'>
|
||||
{sortButtons(buttons! || [ closeButton, okButton ])}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally renders a backdrop that closes the
|
||||
* dialog if clicked.
|
||||
*
|
||||
* @returns {JSX.Element | null}
|
||||
*/
|
||||
public renderBackdrop(): JSX.Element | null {
|
||||
const { isShowingBackdrop } = this.props;
|
||||
|
||||
if (!isShowingBackdrop) return null;
|
||||
|
||||
return (
|
||||
<div key='drop' className='dialogDrop' onClick={this.onClose} />
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isShowing, isCentered, style, children, className, arrow } = this.props;
|
||||
const arrowClass = arrow ? `arrow arrow-${arrow}` : '';
|
||||
const parsedClassName = classNames(
|
||||
'dialog',
|
||||
arrowClass,
|
||||
{ centered: isCentered },
|
||||
className
|
||||
);
|
||||
|
||||
return isShowing ? (
|
||||
<>
|
||||
{this.renderBackdrop()}
|
||||
<div style={style} className={parsedClassName}>
|
||||
{children}
|
||||
{this.renderButtons()}
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppState } from '../state';
|
||||
import { AddVersionDialog } from './dialog-add-version';
|
||||
import { TokenDialog } from './dialog-token';
|
||||
import { WarningDialog } from './dialog-warning';
|
||||
import { Settings } from './settings';
|
||||
import { TokenDialog } from './token-dialog';
|
||||
|
||||
export interface DialogsProps {
|
||||
appState: AppState;
|
||||
@@ -19,18 +21,23 @@ export interface DialogsProps {
|
||||
export class Dialogs extends React.Component<DialogsProps, {}> {
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { isTokenDialogShowing, isSettingsShowing } = appState;
|
||||
const { isTokenDialogShowing, isSettingsShowing, isAddVersionDialogShowing } = appState;
|
||||
const maybeToken = isTokenDialogShowing
|
||||
? <TokenDialog key='dialogs' appState={appState} />
|
||||
: null;
|
||||
const maybeSettings = isSettingsShowing
|
||||
? <Settings key='settings' appState={appState} />
|
||||
: null;
|
||||
const maybeAddLocalVersion = isAddVersionDialogShowing
|
||||
? <AddVersionDialog key='add-version-dialog' appState={appState} />
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div key='dialogs' className='dialogs'>
|
||||
{maybeToken}
|
||||
{maybeSettings}
|
||||
{maybeAddLocalVersion}
|
||||
<WarningDialog appState={appState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ContentNames, getContent } from '../content';
|
||||
import { AppState } from '../state';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
|
||||
|
||||
export interface EditorProps {
|
||||
appState: AppState;
|
||||
monaco: typeof MonacoType;
|
||||
monoacoOptions: MonacoType.editor.IEditorOptions;
|
||||
id: string;
|
||||
options?: Partial<MonacoType.editor.IEditorConstructionOptions>;
|
||||
editorDidMount?: (editor: MonacoType.editor.IStandaloneCodeEditor) => void;
|
||||
@@ -29,14 +28,6 @@ export class Editor extends React.Component<EditorProps> {
|
||||
super(props);
|
||||
|
||||
this.language = props.id === 'html' ? 'html' : 'javascript';
|
||||
this.state = {
|
||||
'options': {
|
||||
'wordWrap': 'off',
|
||||
},
|
||||
'minimap': {
|
||||
'enabled': false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
@@ -44,47 +35,6 @@ export class Editor extends React.Component<EditorProps> {
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
ipcRendererManager.on(IpcEvents.TOGGLE_SOFT_WRAP, async (_event) => {
|
||||
const oldState = this.state;
|
||||
if (this.state['options'].wordWrap === "off") {
|
||||
oldState['options'].wordWrap = "on";
|
||||
|
||||
this.setState(oldState);
|
||||
this.editor.updateOptions({
|
||||
'wordWrap': 'on'
|
||||
});
|
||||
} else if (this.state['options'].wordWrap === "on") {
|
||||
oldState['options'].wordWrap = "off";
|
||||
|
||||
this.setState(oldState);
|
||||
this.editor.updateOptions({
|
||||
'wordWrap': 'off'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipcRendererManager.on(IpcEvents.TOGGLE_MINI_MAP, async (_event) => {
|
||||
const oldState = this.state;
|
||||
if (!this.state['minimap']['enabled']) {
|
||||
oldState['minimap']['enabled'] = true;
|
||||
|
||||
this.setState(oldState);
|
||||
this.editor.updateOptions({
|
||||
minimap: {
|
||||
enabled: true,
|
||||
}
|
||||
});
|
||||
} else if (this.state['minimap']['enabled']) {
|
||||
oldState['minimap']['enabled'] = false;
|
||||
|
||||
this.setState(oldState);
|
||||
this.editor.updateOptions({
|
||||
minimap: {
|
||||
enabled: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this.initMonaco();
|
||||
}
|
||||
|
||||
@@ -112,8 +62,7 @@ export class Editor extends React.Component<EditorProps> {
|
||||
* Initialize Monaco.
|
||||
*/
|
||||
public async initMonaco() {
|
||||
const { monaco, id, appState } = this.props;
|
||||
const { options, minimap } = this.state;
|
||||
const { monaco, id, appState, monoacoOptions } = this.props;
|
||||
const { version } = appState;
|
||||
const ref = this.containerRef.current;
|
||||
|
||||
@@ -121,10 +70,9 @@ export class Editor extends React.Component<EditorProps> {
|
||||
this.editor = monaco.editor.create(ref, {
|
||||
language: this.language,
|
||||
theme: 'main',
|
||||
minimap: minimap,
|
||||
contextmenu: false,
|
||||
value: await getContent(id as ContentNames, version),
|
||||
...options
|
||||
...monoacoOptions
|
||||
});
|
||||
this.editorDidMount(this.editor);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { EditorId } 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 { ipcRendererManager } from '../ipc';
|
||||
import { AppState } from '../state';
|
||||
@@ -22,8 +24,15 @@ const options: any = {
|
||||
},
|
||||
};
|
||||
|
||||
const ViewIdMosaic = Mosaic.ofType<EditorId>();
|
||||
const ViewIdMosaicWindow = MosaicWindow.ofType<EditorId>();
|
||||
const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
wordWrap: 'on'
|
||||
};
|
||||
|
||||
const ViewIdMosaic = Mosaic.ofType<EditorId>() as any;
|
||||
const ViewIdMosaicWindow = MosaicWindow.ofType<EditorId>() as any;
|
||||
|
||||
const TITLE_MAP: Record<EditorId, string> = {
|
||||
main: 'Main Process',
|
||||
@@ -38,13 +47,13 @@ export interface EditorsProps {
|
||||
export interface EditorsState {
|
||||
monaco?: typeof MonacoType;
|
||||
isMounted?: boolean;
|
||||
monacoOptions: MonacoType.editor.IEditorOptions;
|
||||
}
|
||||
|
||||
export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
constructor(props: EditorsProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
this.state = { monacoOptions: defaultMonacoOptions };
|
||||
this.loadMonaco();
|
||||
}
|
||||
|
||||
@@ -56,13 +65,21 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
ipcRendererManager.on(IpcEvents.FS_NEW_FIDDLE, async (_event) => {
|
||||
const { version } = this.props.appState;
|
||||
|
||||
this.props.appState.setWarningDialogTexts({
|
||||
label: 'Your current fiddle is unsaved. Do you want to discard it?'
|
||||
});
|
||||
|
||||
window.ElectronFiddle.app.setValues({
|
||||
html: await getContent(ContentNames.HTML, version),
|
||||
renderer: await getContent(ContentNames.RENDERER, version),
|
||||
main: await getContent(ContentNames.MAIN, version)
|
||||
main: await getContent(ContentNames.MAIN, version),
|
||||
});
|
||||
});
|
||||
|
||||
ipcRendererManager.on(IpcEvents.MONACO_TOGGLE_OPTION, (_event, cmd: string) => {
|
||||
this.toggleEditorOption(cmd);
|
||||
});
|
||||
|
||||
this.setState({ isMounted: true });
|
||||
}
|
||||
|
||||
@@ -86,19 +103,52 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
}
|
||||
}
|
||||
|
||||
public toggleEditorOption(path: string): boolean {
|
||||
if (!window.ElectronFiddle.editors) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monacoOptions } = this.state;
|
||||
const newOptions = { ...monacoOptions };
|
||||
const currentSetting = getAtPath(path, newOptions);
|
||||
|
||||
setAtPath(path, newOptions, toggleMonaco(currentSetting));
|
||||
|
||||
Object.keys(window.ElectronFiddle.editors)
|
||||
.forEach((key) => {
|
||||
const editor: MonacoType.editor.IStandaloneCodeEditor | null
|
||||
= window.ElectronFiddle.editors[key];
|
||||
|
||||
if (editor) {
|
||||
editor.updateOptions(newOptions);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ monacoOptions: newOptions });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Editors: Could not toggle property ${path}`, error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { monaco } = this.state;
|
||||
|
||||
if (!monaco) return null;
|
||||
|
||||
|
||||
return (
|
||||
<ViewIdMosaic
|
||||
onChange={updateEditorLayout}
|
||||
// tslint:disable-next-line:jsx-no-multiline-js
|
||||
renderTile={(id, path) => (
|
||||
renderTile={(id: any, path: any) => (
|
||||
<ViewIdMosaicWindow className={id} path={path} title={TITLE_MAP[id]} toolbarControls={<div />}>
|
||||
<Editor id={id} monaco={monaco} appState={appState} />
|
||||
<Editor id={id} monaco={monaco} appState={appState} monoacoOptions={defaultMonacoOptions} />
|
||||
</ViewIdMosaicWindow>
|
||||
)}
|
||||
initialValue={options}
|
||||
@@ -110,10 +160,8 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
* 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();
|
||||
@@ -123,7 +171,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
|
||||
}
|
||||
|
||||
if (!this.state || !this.state.isMounted) {
|
||||
this.state = { monaco };
|
||||
this.state = { monaco, monacoOptions: defaultMonacoOptions };
|
||||
} else {
|
||||
this.setState({ monaco });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AppState } from '../state';
|
||||
|
||||
export interface CommandsProps {
|
||||
appState: AppState;
|
||||
// Used to keep testing conform
|
||||
renderTimestamp?: (ts: number) => string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,14 +1,18 @@
|
||||
import {
|
||||
faClipboardList,
|
||||
faCloudDownloadAlt,
|
||||
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 } 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';
|
||||
@@ -37,8 +41,8 @@ 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.handleDownloadClick = this.handleDownloadClick.bind(this);
|
||||
this.handleAddVersion = this.handleAddVersion.bind(this);
|
||||
|
||||
this.state = {
|
||||
isDownloadingAll: false,
|
||||
@@ -50,25 +54,13 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
this.props.appState.updateElectronVersions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in how many pages should be displayed.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLSelectElement>} event
|
||||
*/
|
||||
public handlePagesChange(
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
) {
|
||||
const { value } = event.currentTarget;
|
||||
this.props.appState.versionPagesToFetch = parseInt(value, 10) / 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in which channels should be displayed.
|
||||
*
|
||||
* @param {React.ChangeEvent<HTMLInputElement>} event
|
||||
*/
|
||||
public handleChannelChange(
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) {
|
||||
const { id, checked } = event.currentTarget;
|
||||
const { appState } = this.props;
|
||||
@@ -117,47 +109,74 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
this.setState({ isDeletingAll: false });
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isDownloadingAll, isDeletingAll } = this.state;
|
||||
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={faCloudDownloadAlt} />;
|
||||
/**
|
||||
* Opens the "add local version" dialog
|
||||
*/
|
||||
public handleAddVersion(): void {
|
||||
this.props.appState.toggleAddVersionDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className='settings-electron'>
|
||||
<h2>Electron Settings</h2>
|
||||
<div className='advanced-options settings-section'>
|
||||
{this.renderVersionOptions()}
|
||||
|
||||
<label>Download all versions of Electron.</label>
|
||||
<button
|
||||
className='button btn-download-all'
|
||||
disabled={isWorking}
|
||||
onClick={this.handleDownloadAll}
|
||||
>
|
||||
{downloadFontAwesomeIcon} Download All Versions
|
||||
</button>
|
||||
<label>Remove all downloaded versions of Electron.</label>
|
||||
<button
|
||||
className='button btn-delete-all'
|
||||
disabled={isWorking}
|
||||
onClick={this.handleDeleteAll}
|
||||
>
|
||||
{deleteFontAwesomeIcon} Delete All Versions
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Versions</h2>
|
||||
{this.renderVersionOptions()}
|
||||
<br />
|
||||
<Callout>
|
||||
{this.renderAdvancedButtons()}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the various buttons for advanced operations
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderAdvancedButtons(): JSX.Element {
|
||||
const { isDownloadingAll, isDeletingAll } = this.state;
|
||||
const { isUpdatingElectronVersions } = this.props.appState;
|
||||
const isWorking = isDownloadingAll || isDeletingAll;
|
||||
|
||||
return (
|
||||
<ButtonGroup fill={true}>
|
||||
<Button
|
||||
disabled={isUpdatingElectronVersions}
|
||||
onClick={this.handleDownloadClick}
|
||||
loading={isUpdatingElectronVersions}
|
||||
icon='numbered-list'
|
||||
text='Update Electron Release List'
|
||||
/>
|
||||
<Button
|
||||
disabled={isWorking}
|
||||
icon='download'
|
||||
onClick={this.handleDownloadAll}
|
||||
text='Download All Versions'
|
||||
/>
|
||||
<Button
|
||||
disabled={isWorking}
|
||||
icon='trash'
|
||||
onClick={this.handleDeleteAll}
|
||||
text='Delete All Downloads'
|
||||
/>
|
||||
<Button
|
||||
icon='document-open'
|
||||
onClick={this.handleAddVersion}
|
||||
text='Add Local Electron Build'
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the various options for which versions should be displayed
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderVersionOptions(): JSX.Element {
|
||||
const { appState } = this.props;
|
||||
const getIsChecked = (channel: ElectronReleaseChannel) => {
|
||||
@@ -165,78 +184,40 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='versions-to-show'>
|
||||
<p>Include Electron versions from these release channels:</p>
|
||||
<label>
|
||||
<input
|
||||
<Callout>
|
||||
<FormGroup
|
||||
label='Include Electron versions from these release channels:'
|
||||
>
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.stable)}
|
||||
id={ElectronReleaseChannel.stable}
|
||||
type='checkbox'
|
||||
label='Stable'
|
||||
id='Stable'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
Stable
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.beta)}
|
||||
id={ElectronReleaseChannel.beta}
|
||||
type='checkbox'
|
||||
label='Beta'
|
||||
id='Beta'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
Beta
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.nightly)}
|
||||
id={ElectronReleaseChannel.nightly}
|
||||
type='checkbox'
|
||||
label='Nightly'
|
||||
id='Nightly'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
Nightly
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
<Checkbox
|
||||
checked={getIsChecked(ElectronReleaseChannel.unsupported)}
|
||||
id={ElectronReleaseChannel.unsupported}
|
||||
type='checkbox'
|
||||
label='Unsupported'
|
||||
id='Unsupported'
|
||||
onChange={this.handleChannelChange}
|
||||
inline={true}
|
||||
/>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
>
|
||||
{icon} Update Electron Release List
|
||||
</button>
|
||||
</FormGroup>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -248,7 +229,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>
|
||||
@@ -259,7 +243,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
<tbody>
|
||||
{this.renderTableRows()}
|
||||
</tbody>
|
||||
</table>
|
||||
</HTMLTable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -278,21 +262,42 @@ 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';
|
||||
|
||||
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 Electorn 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
|
||||
*
|
||||
@@ -302,36 +307,33 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
private renderAction(key: string, item: ElectronVersion): JSX.Element {
|
||||
const { state } = item;
|
||||
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);
|
||||
|
||||
return (
|
||||
<button className='button' onClick={remove}>
|
||||
<FontAwesomeIcon icon={faTrash} /> Delete
|
||||
</button>
|
||||
);
|
||||
buttonProps.onClick = () => appState.removeVersion(key);
|
||||
buttonProps.icon = 'trash';
|
||||
buttonProps.text = source === ElectronVersionSource.local
|
||||
? 'Remove'
|
||||
: 'Delete';
|
||||
} else if (state === 'downloading') {
|
||||
buttonProps.disabled = true;
|
||||
buttonProps.loading = true;
|
||||
buttonProps.text = 'Downloading';
|
||||
buttonProps.icon = 'cloud-download';
|
||||
} else {
|
||||
buttonProps.disabled = false;
|
||||
buttonProps.loading = false;
|
||||
buttonProps.text = 'Download';
|
||||
buttonProps.icon = 'cloud-download';
|
||||
buttonProps.onClick = () => appState.downloadVersion(key);
|
||||
}
|
||||
|
||||
// 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} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isKeepingUserDataDirs } = 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();
|
||||
|
||||
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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 public 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,3 +1,4 @@
|
||||
import { Divider } from '@blueprintjs/core';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
@@ -22,7 +23,7 @@ export class GeneralSettings extends React.Component<GeneralSettingsProps, {}> {
|
||||
<div>
|
||||
<h2>General Settings</h2>
|
||||
<AppearanceSettings appState={this.props.appState} />
|
||||
<hr />
|
||||
<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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,35 +83,33 @@ 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
|
||||
@@ -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,80 +0,0 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
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 } = item;
|
||||
const version = tag_name;
|
||||
const icon = state === 'ready'
|
||||
? '✅'
|
||||
: state === 'downloading' ? '⏬' : '⏹';
|
||||
|
||||
return (
|
||||
<option value={version} key={version}>
|
||||
{icon} {version}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<select
|
||||
className='select-versions'
|
||||
value={`v${this.props.appState.version}`}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
{this.renderOptions()}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { remote } from 'electron';
|
||||
import * as path from 'path';
|
||||
|
||||
export const USER_DATA_PATH = remote.app.getPath('userData');
|
||||
export const CONFIG_PATH = path.join(remote.app.getPath('home'), '.electron-fiddle');
|
||||
@@ -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');
|
||||
|
||||
@@ -89,8 +89,8 @@ export async function getTypeDefinitions(version: string): Promise<string | null
|
||||
*
|
||||
* @param {string} version
|
||||
*/
|
||||
export async function updateEditorTypeDefinitions(version: string, i: number = 0) {
|
||||
const defer = async () => {
|
||||
export async function updateEditorTypeDefinitions(version: string, i: number = 0): Promise<void> {
|
||||
const defer = async (): Promise<void> => {
|
||||
if (i > 10) {
|
||||
console.warn(`Fetch Types: Failed, dependencies do not exist`);
|
||||
return;
|
||||
@@ -101,7 +101,6 @@ export async function updateEditorTypeDefinitions(version: string, i: number = 0
|
||||
};
|
||||
|
||||
// If this method is called before we're ready, we'll delay this work a bit
|
||||
if (!window.ElectronFiddle) return defer();
|
||||
if (!window.ElectronFiddle.app || !window.ElectronFiddle.app.monaco) return defer();
|
||||
|
||||
const { app } = window.ElectronFiddle;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { shell } from 'electron';
|
||||
import * as fsType from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../constants';
|
||||
import { EditorValues, Files, FileTransform } from '../interfaces';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, 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';
|
||||
@@ -36,13 +35,19 @@ export class FileManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a template.
|
||||
*
|
||||
* @param {string} name
|
||||
* @memberof FileManager
|
||||
*/
|
||||
public async openTemplate(name: string) {
|
||||
const values = await getTemplateValues(name);
|
||||
this.setFiddle(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to open a fiddle
|
||||
* Tries to open a fiddle.
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @memberof FileManager
|
||||
@@ -73,6 +78,12 @@ export class FileManager {
|
||||
this.appState.gistId = '';
|
||||
this.appState.isMyGist = false;
|
||||
this.appState.localPath = filePath || null;
|
||||
|
||||
this.appState.setWarningDialogTexts({
|
||||
label: `Opening this fiddle will replace your unsaved changes. Do you want to proceed?`,
|
||||
ok: 'Yes'
|
||||
});
|
||||
|
||||
await window.ElectronFiddle.app.setValues(values);
|
||||
document.title = getTitle(this.appState);
|
||||
}
|
||||
@@ -84,7 +95,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;
|
||||
|
||||
@@ -96,11 +107,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,53 +1,27 @@
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
|
||||
import { EditorValues, FileTransform } from '../../interfaces';
|
||||
import { IpcEvents } from '../../ipc-events';
|
||||
import { PackageJsonOptions } from '../../utils/get-package';
|
||||
import { normalizeVersion } from '../../utils/normalize-version';
|
||||
import { maybePlural } from '../../utils/plural-maybe';
|
||||
import { ipcRendererManager } from '../ipc';
|
||||
import { findModulesInEditors, getIsNpmInstalled, installModules, npmRun } from '../npm';
|
||||
import { AppState } from '../state';
|
||||
|
||||
export interface RunnerState {
|
||||
isRunning: boolean;
|
||||
}
|
||||
|
||||
export interface RunnerProps {
|
||||
appState: AppState;
|
||||
}
|
||||
import { EditorValues, FileTransform } from '../interfaces';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { getAppDataDir } from '../utils/app-data-dir';
|
||||
import { PackageJsonOptions } from '../utils/get-package';
|
||||
import { maybePlural } from '../utils/plural-maybe';
|
||||
import { ipcRendererManager } from './ipc';
|
||||
import { findModulesInEditors, getIsNpmInstalled, installModules, npmRun } from './npm';
|
||||
import { AppState } from './state';
|
||||
|
||||
export enum ForgeCommands {
|
||||
PACKAGE = 'package',
|
||||
MAKE = 'make'
|
||||
}
|
||||
|
||||
/**
|
||||
* The runner component is responsible for actually launching the fiddle
|
||||
* with Electron. It also renders the button that does so.
|
||||
*
|
||||
* @class Runner
|
||||
* @extends {React.Component<RunnerProps, RunnerState>}
|
||||
*/
|
||||
@observer
|
||||
export class Runner extends React.Component<RunnerProps, RunnerState> {
|
||||
export class Runner {
|
||||
public child: ChildProcess | null = null;
|
||||
|
||||
constructor(props: RunnerProps) {
|
||||
super(props);
|
||||
|
||||
constructor(private readonly appState: AppState) {
|
||||
this.run = this.run.bind(this);
|
||||
this.performForgeOperation = this.performForgeOperation.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
this.state = { isRunning: false };
|
||||
|
||||
this.props.appState.pushOutput('Console ready 🔬');
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
ipcRendererManager.on(IpcEvents.FIDDLE_RUN, this.run);
|
||||
ipcRendererManager.on(IpcEvents.FIDDLE_PACKAGE, () => {
|
||||
this.performForgeOperation(ForgeCommands.PACKAGE);
|
||||
@@ -57,36 +31,50 @@ export class Runner extends React.Component<RunnerProps, RunnerState> {
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { versions, version } = this.props.appState;
|
||||
const { isRunning } = this.state;
|
||||
/**
|
||||
* Actually run the fiddle.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public async run(): Promise<boolean> {
|
||||
const { fileManager, getValues } = window.ElectronFiddle.app;
|
||||
const options = { includeDependencies: false, includeElectron: false };
|
||||
const { binaryManager, currentElectronVersion } = this.appState;
|
||||
const { version, localPath } = currentElectronVersion;
|
||||
|
||||
if (!versions || !version || !versions[normalizeVersion(version)]) {
|
||||
return null;
|
||||
this.appState.isConsoleShowing = true;
|
||||
|
||||
const values = await getValues(options);
|
||||
const dir = await this.saveToTemp(options);
|
||||
|
||||
if (!dir) return false;
|
||||
|
||||
try {
|
||||
await this.installModulesForEditor(values, dir);
|
||||
} catch (error) {
|
||||
console.error('Runner: Could not install modules', error);
|
||||
fileManager.cleanup(dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
const state = versions[normalizeVersion(version)].state;
|
||||
const isReady = await binaryManager.getIsDownloaded(version, localPath);
|
||||
|
||||
let text = 'Run';
|
||||
let action: () => any = this.run;
|
||||
if (!isReady) {
|
||||
console.warn(`Runner: Binary ${version} not ready`);
|
||||
|
||||
if (state === 'downloading') {
|
||||
text = 'Downloading';
|
||||
let message = `Could not start fiddle: `;
|
||||
message += `Electron ${version} not downloaded yet. `;
|
||||
message += `Please wait for it to finish downloading `;
|
||||
message += `before running the fiddle.`;
|
||||
|
||||
this.appState.pushOutput(message, { isNotPre: true });
|
||||
fileManager.cleanup(dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
text = 'Stop';
|
||||
action = this.stop;
|
||||
}
|
||||
this.execute(dir);
|
||||
|
||||
return (
|
||||
<button
|
||||
className='button button-run'
|
||||
onClick={() => action()}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,120 +83,10 @@ export class Runner extends React.Component<RunnerProps, RunnerState> {
|
||||
public async stop() {
|
||||
if (this.child) {
|
||||
this.child.kill();
|
||||
this.setState({
|
||||
isRunning: false
|
||||
});
|
||||
this.appState.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the editor's JavaScript contents for modules
|
||||
* and installs them.
|
||||
*
|
||||
* @param {EditorValues} values
|
||||
* @param {string} dir
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async installModulesForEditor(values: EditorValues, dir: string): Promise<void> {
|
||||
const modules = await findModulesInEditors(values);
|
||||
const { appState } = this.props;
|
||||
|
||||
if (modules && modules.length > 0) {
|
||||
if (!(await getIsNpmInstalled())) {
|
||||
let message = `The ${maybePlural(`module`, modules)} ${modules.join(', ')} need to be installed, `;
|
||||
message += `but we could not find npm. Fiddle requires Node.js and npm `;
|
||||
message += `to support the installation of modules not included in `;
|
||||
message += `Electron. Please visit https://nodejs.org to install Node.js `;
|
||||
message += `and npm.`;
|
||||
|
||||
appState.pushOutput(message, { isNotPre: true });
|
||||
return;
|
||||
}
|
||||
|
||||
appState.pushOutput(`Installing npm modules: ${modules.join(', ')}...`, { isNotPre: true });
|
||||
appState.pushOutput(await installModules({ dir }, ...modules));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Electron.
|
||||
*
|
||||
* @param {string} dir
|
||||
* @param {string} version
|
||||
* @returns {Promise<void>}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async execute(dir: string): Promise<void> {
|
||||
const { appState } = this.props;
|
||||
const { version, pushOutput } = appState;
|
||||
|
||||
const binaryPath = appState.binaryManager.getElectronBinaryPath(version);
|
||||
console.log(`Runner: Binary ${binaryPath} ready, launching`);
|
||||
|
||||
this.child = spawn(binaryPath, [ dir, '--inspect' ]);
|
||||
this.setState({ isRunning: true });
|
||||
pushOutput(`Electron v${version} started.`);
|
||||
|
||||
this.child.stdout.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
|
||||
this.child.stderr.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
|
||||
this.child.on('close', (code) => {
|
||||
const withCode = typeof code === 'number'
|
||||
? ` with code ${code.toString()}.`
|
||||
: `.`;
|
||||
|
||||
pushOutput(`Electron exited${withCode}`);
|
||||
this.setState({ isRunning: false });
|
||||
this.child = null;
|
||||
window.ElectronFiddle.app.fileManager.cleanup(dir);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save files to temp, logging to the Fiddle terminal while doing so
|
||||
*
|
||||
* @param {PackageJsonOptions} options
|
||||
* @param {...Array<FileTransform>} transforms
|
||||
* @returns {(Promise<string | null>)}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async saveToTemp(
|
||||
options: PackageJsonOptions, ...transforms: Array<FileTransform>
|
||||
): Promise<string | null> {
|
||||
const { fileManager } = window.ElectronFiddle.app;
|
||||
const { pushOutput, pushError } = this.props.appState;
|
||||
|
||||
try {
|
||||
pushOutput(`Saving files to temp directory...`);
|
||||
const dir = await fileManager.saveToTemp(options, ...transforms);
|
||||
pushOutput(`Saved files to ${dir}`);
|
||||
return dir;
|
||||
} catch (error) {
|
||||
pushError('Failed to save files.', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs modules in a given directory (we're basically
|
||||
* just running "npm install")
|
||||
*
|
||||
* @param {string} dir
|
||||
* @returns
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async npmInstall(dir: string): Promise<boolean> {
|
||||
try {
|
||||
this.props.appState.pushOutput(`Now running "npm install..."`);
|
||||
this.props.appState.pushOutput(await installModules({ dir }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.props.appState.pushError('Failed to run "npm install".', error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses electron-forge to either package or make the current fiddle
|
||||
*
|
||||
@@ -218,16 +96,15 @@ export class Runner extends React.Component<RunnerProps, RunnerState> {
|
||||
*/
|
||||
public async performForgeOperation(operation: ForgeCommands): Promise<boolean> {
|
||||
const options = { includeDependencies: true, includeElectron: true };
|
||||
const { dotfilesTransform } = await import('../transforms/dotfiles');
|
||||
const { forgeTransform } = await import('../transforms/forge');
|
||||
const { appState } = this.props;
|
||||
const { pushError, pushOutput } = appState;
|
||||
const { dotfilesTransform } = await import('./transforms/dotfiles');
|
||||
const { forgeTransform } = await import('./transforms/forge');
|
||||
const { pushError, pushOutput } = this.appState;
|
||||
|
||||
const strings = operation === ForgeCommands.MAKE
|
||||
? [ 'Creating installers for', 'Binary' ]
|
||||
: [ 'Packaging', 'Installers' ];
|
||||
|
||||
appState.isConsoleShowing = true;
|
||||
this.appState.isConsoleShowing = true;
|
||||
pushOutput(`📦 ${strings[0]} current Fiddle...`);
|
||||
|
||||
if (!(await getIsNpmInstalled())) {
|
||||
@@ -235,7 +112,7 @@ export class Runner extends React.Component<RunnerProps, RunnerState> {
|
||||
message += `to compile packages. Please visit https://nodejs.org to install `;
|
||||
message += `Node.js and npm.`;
|
||||
|
||||
appState.pushOutput(message, { isNotPre: true });
|
||||
this.appState.pushOutput(message, { isNotPre: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -262,47 +139,128 @@ export class Runner extends React.Component<RunnerProps, RunnerState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually run the fiddle.
|
||||
* Analyzes the editor's JavaScript contents for modules
|
||||
* and installs them.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* @param {EditorValues} values
|
||||
* @param {string} dir
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async run(): Promise<boolean> {
|
||||
const { appState } = this.props;
|
||||
const { fileManager, getValues } = window.ElectronFiddle.app;
|
||||
const options = { includeDependencies: false, includeElectron: false };
|
||||
const { binaryManager, version } = appState;
|
||||
public async installModulesForEditor(values: EditorValues, dir: string): Promise<void> {
|
||||
const modules = await findModulesInEditors(values);
|
||||
const { pushOutput } = this.appState;
|
||||
|
||||
appState.isConsoleShowing = true;
|
||||
if (modules && modules.length > 0) {
|
||||
if (!(await getIsNpmInstalled())) {
|
||||
let message = `The ${maybePlural(`module`, modules)} ${modules.join(', ')} need to be installed, `;
|
||||
message += `but we could not find npm. Fiddle requires Node.js and npm `;
|
||||
message += `to support the installation of modules not included in `;
|
||||
message += `Electron. Please visit https://nodejs.org to install Node.js `;
|
||||
message += `and npm.`;
|
||||
|
||||
const isDownloaded = await binaryManager.getIsDownloaded(version);
|
||||
const values = await getValues(options);
|
||||
const dir = await this.saveToTemp(options);
|
||||
pushOutput(message, { isNotPre: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dir) return false;
|
||||
pushOutput(`Installing npm modules: ${modules.join(', ')}...`, { isNotPre: true });
|
||||
pushOutput(await installModules({ dir }, ...modules));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Electron.
|
||||
*
|
||||
* @param {string} dir
|
||||
* @param {string} version
|
||||
* @returns {Promise<void>}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async execute(dir: string): Promise<void> {
|
||||
const { currentElectronVersion, pushOutput, binaryManager } = this.appState;
|
||||
const { version, localPath } = currentElectronVersion;
|
||||
const binaryPath = binaryManager.getElectronBinaryPath(version, localPath);
|
||||
console.log(`Runner: Binary ${binaryPath} ready, launching`);
|
||||
|
||||
this.child = spawn(binaryPath, [ dir, '--inspect' ]);
|
||||
this.appState.isRunning = true;
|
||||
pushOutput(`Electron v${version} started.`);
|
||||
|
||||
this.child.stdout.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
|
||||
this.child.stderr.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
|
||||
this.child.on('close', async (code) => {
|
||||
const withCode = typeof code === 'number'
|
||||
? ` with code ${code.toString()}.`
|
||||
: `.`;
|
||||
|
||||
pushOutput(`Electron exited${withCode}`);
|
||||
this.appState.isRunning = false;
|
||||
this.child = null;
|
||||
|
||||
// Clean older folders
|
||||
await window.ElectronFiddle.app.fileManager.cleanup(dir);
|
||||
await this.deleteUserData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save files to temp, logging to the Fiddle terminal while doing so
|
||||
*
|
||||
* @param {PackageJsonOptions} options
|
||||
* @param {...Array<FileTransform>} transforms
|
||||
* @returns {(Promise<string | null>)}
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async saveToTemp(
|
||||
options: PackageJsonOptions, ...transforms: Array<FileTransform>
|
||||
): Promise<string | null> {
|
||||
const { fileManager } = window.ElectronFiddle.app;
|
||||
const { pushOutput, pushError } = this.appState;
|
||||
|
||||
try {
|
||||
await this.installModulesForEditor(values, dir);
|
||||
pushOutput(`Saving files to temp directory...`);
|
||||
const dir = await fileManager.saveToTemp(options, ...transforms);
|
||||
pushOutput(`Saved files to ${dir}`);
|
||||
return dir;
|
||||
} catch (error) {
|
||||
console.error('Runner: Could not install modules', error);
|
||||
fileManager.cleanup(dir);
|
||||
return false;
|
||||
pushError('Failed to save files.', error);
|
||||
}
|
||||
|
||||
if (!isDownloaded) {
|
||||
console.warn(`Runner: Binary ${version} not ready`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let message = `Could not start fiddle: `;
|
||||
message += `Electron ${version} not downloaded yet. `;
|
||||
message += `Please wait for it to finish downloading `;
|
||||
message += `before running the fiddle.`;
|
||||
|
||||
appState.pushOutput(message, { isNotPre: true });
|
||||
fileManager.cleanup(dir);
|
||||
return false;
|
||||
/**
|
||||
* Installs modules in a given directory (we're basically
|
||||
* just running "npm install")
|
||||
*
|
||||
* @param {string} dir
|
||||
* @returns
|
||||
* @memberof Runner
|
||||
*/
|
||||
public async npmInstall(dir: string): Promise<boolean> {
|
||||
try {
|
||||
this.appState.pushOutput(`Now running "npm install..."`);
|
||||
this.appState.pushOutput(await installModules({ dir }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.appState.pushError('Failed to run "npm install".', error);
|
||||
}
|
||||
|
||||
this.execute(dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
/**
|
||||
* Deletes the user data dir after a run.
|
||||
*/
|
||||
private async deleteUserData() {
|
||||
if (this.appState.isKeepingUserDataDirs) {
|
||||
console.log(`Cleanup: Not deleting data dir due to isKeepingUserDataDirs setting`);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = await this.appState.getName();
|
||||
const appData = getAppDataDir(name);
|
||||
|
||||
console.log(`Cleanup: Deleting data dir ${appData}`);
|
||||
await window.ElectronFiddle.app.fileManager.cleanup(appData);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
import { action, autorun, observable } from 'mobx';
|
||||
import { action, autorun, computed, observable, when } from 'mobx';
|
||||
|
||||
import { ElectronVersion, OutputEntry, OutputOptions } from '../interfaces';
|
||||
import {
|
||||
ElectronVersion,
|
||||
ElectronVersionSource,
|
||||
ElectronVersionState,
|
||||
NpmVersion,
|
||||
OutputEntry,
|
||||
OutputOptions,
|
||||
WarningDialogTexts
|
||||
} from '../interfaces';
|
||||
import { IpcEvents } from '../ipc-events';
|
||||
import { arrayToStringMap } from '../utils/array-to-stringmap';
|
||||
import { getName } from '../utils/get-title';
|
||||
@@ -10,18 +18,24 @@ import { ContentNames, getContent, isContentUnchanged } from './content';
|
||||
import { updateEditorTypeDefinitions } from './fetch-types';
|
||||
import { ipcRendererManager } from './ipc';
|
||||
import { activateTheme } from './themes';
|
||||
import { ElectronReleaseChannel, getKnownVersions, getUpdatedKnownVersions } from './versions';
|
||||
import {
|
||||
addLocalVersion,
|
||||
ElectronReleaseChannel,
|
||||
getDefaultVersion,
|
||||
getElectronVersions,
|
||||
getUpdatedElectronVersions,
|
||||
saveLocalVersions
|
||||
} from './versions';
|
||||
|
||||
const knownVersions = getKnownVersions();
|
||||
const defaultVersion = localStorage.getItem('version')
|
||||
|| normalizeVersion(knownVersions[0].tag_name);
|
||||
const knownVersions = getElectronVersions();
|
||||
const defaultVersion = getDefaultVersion(knownVersions);
|
||||
|
||||
/**
|
||||
* Editors exist outside of React's world. To make things *a lot*
|
||||
* easier, we keep them around in a global object. Don't judge us,
|
||||
* we're really only doing that for the editors.
|
||||
*/
|
||||
window.ElectronFiddle = {
|
||||
window.ElectronFiddle = window.ElectronFiddle || {
|
||||
editors: {
|
||||
main: null,
|
||||
renderer: null,
|
||||
@@ -37,35 +51,39 @@ window.ElectronFiddle = {
|
||||
* @class AppState
|
||||
*/
|
||||
export class AppState {
|
||||
// Persisted settings
|
||||
// -- Persisted settings ------------------
|
||||
@observable public version: string = defaultVersion;
|
||||
@observable public theme: string | null = localStorage.getItem('theme');
|
||||
@observable public gitHubAvatarUrl: string | null = localStorage.getItem('gitHubAvatarUrl');
|
||||
@observable public gitHubName: string | null = localStorage.getItem('gitHubName');
|
||||
@observable public gitHubLogin: string | null = localStorage.getItem('gitHubLogin');
|
||||
@observable public gitHubToken: string | null = localStorage.getItem('gitHubToken') || null;
|
||||
@observable public versionPagesToFetch: number = parseInt(
|
||||
localStorage.getItem('versionPagesToFetch') || '2', 10
|
||||
);
|
||||
@observable public gitHubPublishAsPublic: boolean = !!this.retrieve('gitHubPublishAsPublic');
|
||||
@observable public versionsToShow: Array<ElectronReleaseChannel> =
|
||||
this.retrieve('versionsToShow', true) as Array<ElectronReleaseChannel>
|
||||
this.retrieve('versionsToShow') as Array<ElectronReleaseChannel>
|
||||
|| [ ElectronReleaseChannel.stable, ElectronReleaseChannel.beta ];
|
||||
@observable public isKeepingUserDataDirs: boolean = !!this.retrieve('isKeepingUserDataDirs');
|
||||
|
||||
@observable public binaryManager: BinaryManager = new BinaryManager();
|
||||
|
||||
// Various session-only state
|
||||
// -- Various session-only state ------------------
|
||||
@observable public gistId: string = '';
|
||||
@observable public isMyGist: boolean = false;
|
||||
@observable public versions: Record<string, ElectronVersion> = arrayToStringMap(knownVersions);
|
||||
@observable public output: Array<OutputEntry> = [];
|
||||
@observable public localPath: string | null = null;
|
||||
@observable public isUpdatingElectronVersions = false;
|
||||
@observable public warningDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
|
||||
@observable public warningDialogLastResult: boolean | null = null;
|
||||
@observable public isRunning = false;
|
||||
|
||||
// Various "isShowing" settings
|
||||
// -- Various "isShowing" settings ------------------
|
||||
@observable public isConsoleShowing: boolean = false;
|
||||
@observable public isTokenDialogShowing: boolean = false;
|
||||
@observable public isWarningDialogShowing: boolean = false;
|
||||
@observable public isSettingsShowing: boolean = false;
|
||||
@observable public isUnsaved: boolean = false;
|
||||
@observable public isAddVersionDialogShowing: boolean = false;
|
||||
@observable public isTourShowing: boolean = !localStorage.getItem('hasShownTour');
|
||||
|
||||
private outputBuffer: string = '';
|
||||
@@ -88,24 +106,37 @@ export class AppState {
|
||||
ipcRendererManager.on(IpcEvents.OPEN_SETTINGS, this.toggleSettings);
|
||||
ipcRendererManager.on(IpcEvents.SHOW_WELCOME_TOUR, this.showTour);
|
||||
|
||||
// Setup autoruns
|
||||
// Setup auto-runs
|
||||
autorun(() => this.save('theme', this.theme));
|
||||
autorun(() => this.save('gitHubAvatarUrl', this.gitHubAvatarUrl));
|
||||
autorun(() => this.save('gitHubLogin', this.gitHubLogin));
|
||||
autorun(() => this.save('gitHubName', this.gitHubName));
|
||||
autorun(() => this.save('gitHubToken', this.gitHubToken));
|
||||
autorun(() => this.save('gitHubPublishAsPublic', this.gitHubPublishAsPublic));
|
||||
autorun(() => this.save('isKeepingUserDataDirs', this.isKeepingUserDataDirs));
|
||||
autorun(() => this.save('version', this.version));
|
||||
autorun(() => this.save('versionPagesToFetch', this.versionPagesToFetch));
|
||||
autorun(() => this.save('versionsToShow', this.versionsToShow));
|
||||
|
||||
autorun(() => {
|
||||
if (this.isUnsaved) {
|
||||
window.onbeforeunload = () => {
|
||||
const result = !confirm('The current Fiddle is unsaved. Do you want to exit anyway?');
|
||||
this.setWarningDialogTexts({
|
||||
label: `The current Fiddle is unsaved. Do you want to exit anyway?`,
|
||||
ok: 'Quit'
|
||||
});
|
||||
|
||||
if (result) {
|
||||
window.close();
|
||||
}
|
||||
this.isWarningDialogShowing = true;
|
||||
|
||||
// We'll wait until the warning dialog was closed
|
||||
when(() => !this.isWarningDialogShowing).then(() => {
|
||||
// The user confirmed, let's close for real.
|
||||
if (this.warningDialogLastResult) {
|
||||
window.onbeforeunload = null;
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
} else {
|
||||
window.onbeforeunload = null;
|
||||
@@ -114,21 +145,38 @@ export class AppState {
|
||||
|
||||
// Update our known versions
|
||||
this.updateElectronVersions();
|
||||
|
||||
// Make sure the console isn't all empty and sad
|
||||
this.pushOutput('Console ready 🔬');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current ElectronVersion or the first
|
||||
* one that can be found.
|
||||
*/
|
||||
@computed get currentElectronVersion(): ElectronVersion {
|
||||
if (this.versions[this.version]) {
|
||||
return this.versions[this.version];
|
||||
} else {
|
||||
return this.versions[defaultVersion];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the Electron versions: First, fetch them from GitHub,
|
||||
* then update their respective downloaded state.
|
||||
*
|
||||
* Fails silently.
|
||||
*/
|
||||
@action public async updateElectronVersions() {
|
||||
this.isUpdatingElectronVersions = true;
|
||||
|
||||
try {
|
||||
const versions = await getUpdatedKnownVersions(this.versionPagesToFetch);
|
||||
const versions = await getUpdatedElectronVersions();
|
||||
this.versions = arrayToStringMap(versions);
|
||||
await this.updateDownloadedVersionState();
|
||||
} catch (error) {
|
||||
console.warn(`State: Could not update Electron versions`);
|
||||
console.warn(`State: Could not update Electron versions`, error);
|
||||
}
|
||||
|
||||
this.isUpdatingElectronVersions = false;
|
||||
@@ -146,10 +194,22 @@ export class AppState {
|
||||
this.isConsoleShowing = !this.isConsoleShowing;
|
||||
}
|
||||
|
||||
@action public toggleAddVersionDialog() {
|
||||
this.isAddVersionDialogShowing = !this.isAddVersionDialogShowing;
|
||||
}
|
||||
|
||||
@action public toggleAuthDialog() {
|
||||
this.isTokenDialogShowing = !this.isTokenDialogShowing;
|
||||
}
|
||||
|
||||
@action public toogleWarningDialog() {
|
||||
this.isWarningDialogShowing = !this.isWarningDialogShowing;
|
||||
|
||||
if (this.isWarningDialogShowing) {
|
||||
this.warningDialogLastResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
@action public toggleSettings() {
|
||||
// We usually don't lose editor focus,
|
||||
// so you can still type. Let's force-blur.
|
||||
@@ -169,13 +229,28 @@ export class AppState {
|
||||
this.resetView({ isTourShowing: true });
|
||||
}
|
||||
|
||||
@action public setTheme(name?: string) {
|
||||
this.theme = name || '';
|
||||
activateTheme(undefined, undefined, name);
|
||||
@action public setTheme(fileName?: string) {
|
||||
this.theme = fileName || '';
|
||||
activateTheme(undefined, undefined, fileName);
|
||||
window.ElectronFiddle.app.setupTheme();
|
||||
}
|
||||
|
||||
/*
|
||||
@action public setWarningDialogTexts(input: WarningDialogTexts) {
|
||||
this.warningDialogTexts = {
|
||||
ok: 'Okay',
|
||||
cancel: 'Cancel',
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
@action public addLocalVersion(input: NpmVersion) {
|
||||
addLocalVersion(input);
|
||||
|
||||
this.versions = arrayToStringMap(getElectronVersions());
|
||||
this.updateDownloadedVersionState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a version of Electron
|
||||
*
|
||||
* @param {string} input
|
||||
@@ -183,6 +258,8 @@ export class AppState {
|
||||
*/
|
||||
@action public async removeVersion(input: string) {
|
||||
const version = normalizeVersion(input);
|
||||
const release = this.versions[version];
|
||||
|
||||
console.log(`State: Removing Electron ${version}`);
|
||||
|
||||
// Already not present?
|
||||
@@ -191,18 +268,28 @@ export class AppState {
|
||||
return;
|
||||
}
|
||||
|
||||
// Actually remove
|
||||
await this.binaryManager.remove(version);
|
||||
|
||||
// Update state
|
||||
const updatedVersions = { ...this.versions };
|
||||
updatedVersions[version].state = 'unknown';
|
||||
|
||||
// Actually remove
|
||||
if (release && release.source === ElectronVersionSource.local) {
|
||||
delete updatedVersions[version];
|
||||
|
||||
const versionsAsArray = Object
|
||||
.keys(updatedVersions)
|
||||
.map((k) => updatedVersions[k]);
|
||||
|
||||
saveLocalVersions(versionsAsArray);
|
||||
} else {
|
||||
await this.binaryManager.remove(version);
|
||||
updatedVersions[version].state = ElectronVersionState.unknown;
|
||||
}
|
||||
|
||||
this.versions = updatedVersions;
|
||||
this.updateDownloadedVersionState();
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Download a version of Electron.
|
||||
*
|
||||
* @param {string} input
|
||||
@@ -212,12 +299,16 @@ export class AppState {
|
||||
const version = normalizeVersion(input);
|
||||
console.log(`State: Downloading Electron ${version}`);
|
||||
|
||||
const release = this.versions[version] || { state: '', source: '' };
|
||||
const isLocal = release.source === ElectronVersionSource.local;
|
||||
const isReady = release.state === 'ready';
|
||||
|
||||
// Fetch new binaries, maybe?
|
||||
if ((this.versions[version] || { state: '' }).state !== 'ready') {
|
||||
if (!isLocal && !isReady) {
|
||||
console.log(`State: Instructing BinaryManager to fetch v${version}`);
|
||||
const updatedVersions = { ...this.versions };
|
||||
updatedVersions[version] = updatedVersions[version] || {};
|
||||
updatedVersions[version].state = 'downloading';
|
||||
updatedVersions[version].state = ElectronVersionState.downloading;
|
||||
this.versions = updatedVersions;
|
||||
|
||||
await this.binaryManager.setup(version);
|
||||
@@ -227,7 +318,7 @@ export class AppState {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Select a version of Electron (and download it if necessary).
|
||||
*
|
||||
* @param {string} input
|
||||
@@ -235,6 +326,14 @@ export class AppState {
|
||||
*/
|
||||
@action public async setVersion(input: string) {
|
||||
const version = normalizeVersion(input);
|
||||
|
||||
if (!this.versions[version]) {
|
||||
console.warn(`State: Called setVersion() with ${version}, which does not exist.`);
|
||||
this.setVersion(knownVersions[0].version);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`State: Switching to Electron ${version}`);
|
||||
|
||||
this.version = version;
|
||||
@@ -242,7 +341,7 @@ export class AppState {
|
||||
// Should we update the editor?
|
||||
if (await isContentUnchanged(ContentNames.MAIN)) {
|
||||
const main = await getContent(ContentNames.MAIN, version);
|
||||
window.ElectronFiddle.app.setValues({ main });
|
||||
window.ElectronFiddle.app.setValues({ main }, false);
|
||||
}
|
||||
|
||||
// Update TypeScript definitions
|
||||
@@ -252,7 +351,7 @@ export class AppState {
|
||||
await this.downloadVersion(version);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Go and check which versions have already been downloaded.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
@@ -261,12 +360,12 @@ export class AppState {
|
||||
const downloadedVersions = await this.binaryManager.getDownloadedVersions();
|
||||
const updatedVersions = { ...this.versions };
|
||||
|
||||
console.log(`State: Updating version state`);
|
||||
(downloadedVersions || []).forEach((version) => {
|
||||
if (updatedVersions[version]) {
|
||||
updatedVersions[version].state = 'ready';
|
||||
updatedVersions[version].state = ElectronVersionState.ready;
|
||||
}
|
||||
});
|
||||
console.log(`State: Updated version state`, updatedVersions);
|
||||
|
||||
this.versions = updatedVersions;
|
||||
}
|
||||
@@ -347,6 +446,7 @@ export class AppState {
|
||||
this.isSettingsShowing = false;
|
||||
this.isTourShowing = false;
|
||||
this.isConsoleShowing = false;
|
||||
this.isAddVersionDialogShowing = false;
|
||||
|
||||
if (additionalOptions) {
|
||||
for (const key in additionalOptions) {
|
||||
@@ -382,7 +482,7 @@ export class AppState {
|
||||
* @param {string} key
|
||||
* @param {(string | number | object)} [value]
|
||||
*/
|
||||
private save(key: string, value?: string | number | object | null) {
|
||||
private save(key: string, value?: string | number | object | null | boolean) {
|
||||
if (value) {
|
||||
const _value = typeof value === 'object'
|
||||
? JSON.stringify(value)
|
||||
@@ -399,17 +499,12 @@ export class AppState {
|
||||
*
|
||||
* @template T
|
||||
* @param {string} key
|
||||
* @param {boolean} parse
|
||||
* @returns {(T | string | null)}
|
||||
*/
|
||||
private retrieve<T>(key: string, parse: boolean): T | string | null {
|
||||
private retrieve<T>(key: string): T | string | null {
|
||||
const value = localStorage.getItem(key);
|
||||
|
||||
if (parse) {
|
||||
return JSON.parse(value || 'null') as T;
|
||||
}
|
||||
|
||||
return value;
|
||||
return JSON.parse(value || 'null') as T;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as fsExtraType from 'fs-extra';
|
||||
import * as pathType from 'path';
|
||||
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../constants';
|
||||
import { EditorValues } from '../interfaces';
|
||||
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
|
||||
import { fancyImport } from '../utils/import';
|
||||
|
||||
/**
|
||||
@@ -16,7 +16,7 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
|
||||
const path = await fancyImport<typeof pathType>('path');
|
||||
const fs = await fancyImport<typeof fsExtraType>('fs-extra');
|
||||
|
||||
const templatePath = path.join(__dirname, '../static/templates', name);
|
||||
const templatePath = path.join(__dirname, '../../static/templates', name.toLowerCase());
|
||||
|
||||
const getFile = async (fileName: string) => {
|
||||
try {
|
||||
@@ -25,6 +25,7 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
|
||||
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.warn(`Could not get template file:`, error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,14 @@ import * as MonacoType from 'monaco-editor';
|
||||
|
||||
export interface FiddleTheme {
|
||||
name?: string;
|
||||
isDark?: boolean;
|
||||
editor: Partial<MonacoType.editor.IStandaloneThemeData>;
|
||||
common: {
|
||||
'fonts-common': string;
|
||||
'foreground-1': string;
|
||||
'foreground-2': string;
|
||||
'foreground-3': string;
|
||||
'background-4': string;
|
||||
'background-3': string;
|
||||
'background-2': string;
|
||||
'background-1': string;
|
||||
@@ -34,10 +36,12 @@ export interface LoadedFiddleTheme extends FiddleTheme {
|
||||
export const defaultDark: LoadedFiddleTheme = {
|
||||
name: 'Fiddle (Dark)',
|
||||
file: 'defaultDark',
|
||||
isDark: true,
|
||||
common: {
|
||||
'foreground-1': '#9feafa',
|
||||
'foreground-2': '#8ac7d6',
|
||||
'foreground-3': '#608291',
|
||||
'background-4': '#21232d',
|
||||
'background-3': '#2c2e3b',
|
||||
'background-2': '#1d2427',
|
||||
'background-1': '#2f3241',
|
||||
@@ -65,10 +69,12 @@ export const defaultDark: LoadedFiddleTheme = {
|
||||
export const defaultLight: LoadedFiddleTheme = {
|
||||
name: 'Fiddle (Light)',
|
||||
file: 'defaultLight',
|
||||
isDark: false,
|
||||
common: {
|
||||
'foreground-1': '#9feafa',
|
||||
'foreground-2': '#256e80',
|
||||
'foreground-3': '#608291',
|
||||
'background-4': '#fbfbfb',
|
||||
'background-3': '#fbfbfb',
|
||||
'background-2': '#d6dde0',
|
||||
'background-1': '#f5f5f5',
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as fsType from 'fs-extra';
|
||||
import * as MonacoType from 'monaco-editor';
|
||||
import * as path from 'path';
|
||||
|
||||
import { CONFIG_PATH } from '../constants';
|
||||
import { fancyImport } from '../utils/import';
|
||||
import { CONFIG_PATH } from './constants';
|
||||
import { defaultDark, defaultLight, DefaultThemes, FiddleTheme, LoadedFiddleTheme } from './themes-defaults';
|
||||
|
||||
export const THEMES_PATH = path.join(CONFIG_PATH, 'themes');
|
||||
@@ -20,7 +20,7 @@ export async function activateTheme(
|
||||
const _monaco = monaco || window.ElectronFiddle.app.monaco;
|
||||
const _theme = theme || await getTheme(name);
|
||||
|
||||
if (!_monaco) return;
|
||||
if (!_monaco || !_monaco.editor) return;
|
||||
|
||||
_monaco.editor.defineTheme('main', _theme.editor as any);
|
||||
_monaco.editor.setTheme('main');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PACKAGE_NAME } from '../../constants';
|
||||
import { Files } from '../../interfaces';
|
||||
import { PACKAGE_NAME } from '../../shared-constants';
|
||||
|
||||
/**
|
||||
* This transform turns the files into an electron-forge
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GitHubVersion } from '../interfaces';
|
||||
import { ElectronVersion, ElectronVersionSource, ElectronVersionState, NpmVersion } from '../interfaces';
|
||||
import { normalizeVersion } from '../utils/normalize-version';
|
||||
|
||||
export const enum ElectronReleaseChannel {
|
||||
stable = 'Stable',
|
||||
@@ -7,17 +8,50 @@ export const enum ElectronReleaseChannel {
|
||||
unsupported = 'Unsupported'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sensible default version string.
|
||||
*
|
||||
* @param {Array<ElectronVersion>} knownVersions
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDefaultVersion(
|
||||
knownVersions: Array<ElectronVersion> = []
|
||||
): string {
|
||||
const ls = localStorage.getItem('version');
|
||||
|
||||
if (ls && knownVersions && knownVersions.find(({ version }) => version === ls)) {
|
||||
return ls;
|
||||
}
|
||||
|
||||
// Self-heal: Version not formated correctly
|
||||
const normalized = ls && normalizeVersion(ls);
|
||||
if (normalized) {
|
||||
if (knownVersions && knownVersions.find(({ version }) => version === normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
// Alright, the first version?
|
||||
const last = knownVersions && knownVersions[knownVersions.length - 1];
|
||||
if (last) {
|
||||
return last.version;
|
||||
}
|
||||
|
||||
// Report error
|
||||
throw new Error('Corrupted version data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the release channel for a given input
|
||||
* version.
|
||||
*
|
||||
* @param {GitHubVersion} input
|
||||
* @param {NpmVersion} input
|
||||
* @returns {ElectronReleaseChannel}
|
||||
*/
|
||||
export function getReleaseChannel(
|
||||
input: GitHubVersion
|
||||
input: NpmVersion
|
||||
): ElectronReleaseChannel {
|
||||
const tag = input.tag_name || '';
|
||||
const tag = input.version || '';
|
||||
|
||||
if (tag.includes('beta')) {
|
||||
return ElectronReleaseChannel.beta;
|
||||
@@ -35,34 +69,146 @@ export function getReleaseChannel(
|
||||
return ElectronReleaseChannel.stable;
|
||||
}
|
||||
|
||||
export const enum VersionKeys {
|
||||
local = 'local-electron-versions',
|
||||
known = 'known-electron-versions'
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve Electron versions from localStorage.
|
||||
*
|
||||
* @param {VersionKeys} key
|
||||
* @param {() => Array<NpmVersion>} fallbackMethod
|
||||
* @returns {Array<NpmVersion>}
|
||||
*/
|
||||
function getVersions(
|
||||
key: VersionKeys, fallbackMethod: () => Array<NpmVersion>
|
||||
): Array<NpmVersion> {
|
||||
const fromLs = window.localStorage.getItem(key);
|
||||
|
||||
if (fromLs) {
|
||||
try {
|
||||
let result: Array<NpmVersion> = JSON.parse(fromLs);
|
||||
|
||||
if (!isExpectedFormat(result)) {
|
||||
// Known versions can just be downloaded again.
|
||||
if (key === VersionKeys.known) {
|
||||
throw new Error(`Electron versions in LS does not match expected format`);
|
||||
}
|
||||
|
||||
// Local versions are a bit more tricky and might be in an old format (pre 0.5)
|
||||
result = migrateVersions(result);
|
||||
saveLocalVersions(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.warn(`Parsing local Electron versions failed, returning fallback method.`);
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackMethod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an array of GitHubVersions to localStorage.
|
||||
*
|
||||
* @param {VersionKeys} key
|
||||
* @param {Array<NpmVersion} versions
|
||||
*/
|
||||
function saveVersions(key: VersionKeys, versions: Array<NpmVersion>) {
|
||||
const stringified = JSON.stringify(versions);
|
||||
window.localStorage.setItem(key, stringified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return both known as well as local versions.
|
||||
*
|
||||
* @returns {Array<NpmVersion>}
|
||||
*/
|
||||
export function getElectronVersions(): Array<ElectronVersion> {
|
||||
const known: Array<ElectronVersion> = getKnownVersions().map((version) => {
|
||||
return {
|
||||
...version,
|
||||
source: ElectronVersionSource.remote,
|
||||
state: ElectronVersionState.unknown
|
||||
};
|
||||
});
|
||||
|
||||
const local: Array<ElectronVersion> = getLocalVersions().map((version) => {
|
||||
return {
|
||||
...version,
|
||||
source: ElectronVersionSource.local,
|
||||
state: ElectronVersionState.ready
|
||||
};
|
||||
});
|
||||
|
||||
return [ ...known, ...local ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a version to the local versions
|
||||
*
|
||||
* @param {NpmVersion} input
|
||||
* @returns {Array<NpmVersion>}
|
||||
*/
|
||||
export function addLocalVersion(input: NpmVersion): Array<NpmVersion> {
|
||||
const versions = getLocalVersions();
|
||||
|
||||
if (!versions.find((v) => v.localPath === input.localPath)) {
|
||||
versions.push(input);
|
||||
}
|
||||
|
||||
saveLocalVersions(versions);
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves local Electron versions, configured by the user.
|
||||
*
|
||||
* @returns {Array<NpmVersion>}
|
||||
*/
|
||||
export function getLocalVersions(): Array<NpmVersion> {
|
||||
const versions = getVersions(VersionKeys.local, () => []);
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves local versions to localStorage.
|
||||
*
|
||||
* @param {Array<NpmVersion>} versions
|
||||
*/
|
||||
export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>) {
|
||||
const filteredVersions = versions.filter((v) => {
|
||||
if (isElectronVersion(v)) {
|
||||
return v.source === ElectronVersionSource.local;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return saveVersions(VersionKeys.local, filteredVersions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves our best guess regarding the latest Electron versions. Tries to
|
||||
* fetch them from localStorage, then from a static releases.json file.
|
||||
*
|
||||
* @returns {Array<GitHubVersion>}
|
||||
* @returns {Array<NpmVersion>}
|
||||
*/
|
||||
export function getKnownVersions(): Array<GitHubVersion> {
|
||||
const fromLs = window.localStorage.getItem('known-electron-versions');
|
||||
|
||||
if (fromLs) {
|
||||
try {
|
||||
return JSON.parse(fromLs);
|
||||
} catch (error) {
|
||||
console.warn(`Parsing known versions failed, falling back`);
|
||||
}
|
||||
}
|
||||
|
||||
return require('../../static/releases.json');
|
||||
export function getKnownVersions(): Array<NpmVersion> {
|
||||
return getVersions(VersionKeys.known, () => require('../../static/releases.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves known versions to localStorage.
|
||||
*
|
||||
* @param {Array<GitHubVersion>} versions
|
||||
* @param {Array<NpmVersion>} versions
|
||||
*/
|
||||
export function saveKnownVersions(versions: Array<GitHubVersion>) {
|
||||
const stringified = JSON.stringify(versions);
|
||||
window.localStorage.setItem('known-electron-versions', stringified);
|
||||
export function saveKnownVersions(versions: Array<NpmVersion>) {
|
||||
return saveVersions(VersionKeys.known, versions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,52 +216,35 @@ export function saveKnownVersions(versions: Array<GitHubVersion>) {
|
||||
* saved after.
|
||||
*
|
||||
* @export
|
||||
* @returns {Promise<Array<GitHubVersion>>}
|
||||
* @returns {Promise<Array<ElectronVersion>>}
|
||||
*/
|
||||
export async function getUpdatedKnownVersions(
|
||||
pages: number,
|
||||
): Promise<Array<GitHubVersion>> {
|
||||
export async function getUpdatedElectronVersions(
|
||||
): Promise<Array<ElectronVersion>> {
|
||||
try {
|
||||
await fetchVersions(pages);
|
||||
await fetchVersions();
|
||||
} catch (error) {
|
||||
console.warn(`Versions: Failed to fetch versions`, { error });
|
||||
}
|
||||
|
||||
return getKnownVersions();
|
||||
return getElectronVersions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest known versions directly from GitHub.
|
||||
* Fetch the latest known versions directly from npm.
|
||||
*
|
||||
* @returns {Promise<Array<GitHubVersion>>}
|
||||
* @returns {Promise<Array<NpmVersion>>}
|
||||
*/
|
||||
export async function fetchVersions(pages: number) {
|
||||
const output: Array<GitHubVersion> = [];
|
||||
export async function fetchVersions() {
|
||||
const response = await window.fetch(`https://registry.npmjs.org/electron`);
|
||||
const data = await response.json();
|
||||
const versions: Record<string, any> = data.versions;
|
||||
|
||||
for (let i = 0; i < pages; i++) {
|
||||
const page = await fetchVersionPage(i + 1);
|
||||
const output: Array<NpmVersion> = Object
|
||||
.keys(versions)
|
||||
.map((version) => ({ version }));
|
||||
|
||||
if (page && page.length > 0 && page[0].tag_name) {
|
||||
page.forEach((release) => {
|
||||
const updated = {
|
||||
url: release.url,
|
||||
assets_url: release.assets_url,
|
||||
html_url: release.html_url,
|
||||
tag_name: release.tag_name,
|
||||
target_commitish: release.target_commitish,
|
||||
name: release.name,
|
||||
prerelease: release.prerelease,
|
||||
created_at: release.created_at,
|
||||
published_at: release.published_at,
|
||||
body: release.body
|
||||
};
|
||||
|
||||
output.push(updated as GitHubVersion);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (output && output.length > 0) {
|
||||
if (output && output.length > 0 && isExpectedFormat(output)) {
|
||||
console.log(`Fetched new Electron versions (Count: ${output.length})`);
|
||||
saveKnownVersions(output);
|
||||
}
|
||||
|
||||
@@ -123,14 +252,40 @@ export async function fetchVersions(pages: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single releases page from GitHub.
|
||||
* Is the given array an array of versions?
|
||||
*
|
||||
* @returns {Promise<Array<GitHubVersion>>}
|
||||
* @param {Array<any>} input
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function fetchVersionPage(page?: number): Promise<Array<GitHubVersion>> {
|
||||
const url = `https://api.github.com/repos/electron/electron/releases`
|
||||
+ ((page && page > 1) ? `?page=${page}` : '');
|
||||
export function isExpectedFormat(input: Array<any>): boolean {
|
||||
return input.every((entry) => !!entry.version);
|
||||
}
|
||||
|
||||
return window.fetch(url)
|
||||
.then((response) => response.json());
|
||||
/**
|
||||
* Migrates old versions, if necessary
|
||||
*
|
||||
* @param {Array<any>} input
|
||||
* @returns {Array<NpmVersion>}
|
||||
*/
|
||||
export function migrateVersions(input: Array<any> = []): Array<NpmVersion> {
|
||||
return input
|
||||
.filter((item) => !!item)
|
||||
.map((item) => {
|
||||
const { tag_name, name, url } = item;
|
||||
|
||||
if (!tag_name || !name || !url) return null;
|
||||
|
||||
return {
|
||||
version: tag_name,
|
||||
name,
|
||||
localPath: url
|
||||
};
|
||||
})
|
||||
.filter((item) => !!item) as Array<NpmVersion>;
|
||||
}
|
||||
|
||||
export function isElectronVersion(
|
||||
input: NpmVersion | ElectronVersion
|
||||
): input is ElectronVersion {
|
||||
return (input as ElectronVersion).source !== undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
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';
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import { remote } from 'electron';
|
||||
|
||||
let appData = '';
|
||||
|
||||
/**
|
||||
* Returns an appData path for a given input
|
||||
*
|
||||
* @param input {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAppDataDir(input: string): string {
|
||||
appData = appData || remote.app.getPath('appData');
|
||||
return path.join(appData, input);
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
import { ElectronVersion, GitHubVersion } from '../interfaces';
|
||||
import { ElectronVersion } from '../interfaces';
|
||||
import { normalizeVersion } from './normalize-version';
|
||||
|
||||
/**
|
||||
* Takes an array of GitHub releases and returns a StringMap of
|
||||
* Electron releases.
|
||||
*
|
||||
* @param {Array<GitHubVersion>} input
|
||||
* @param {Array<ElectronVersion>} input
|
||||
* @returns {Record<string, ElectronVersion>}
|
||||
*/
|
||||
export function arrayToStringMap(
|
||||
input: Array<GitHubVersion>
|
||||
input: Array<ElectronVersion>
|
||||
): Record<string, ElectronVersion> {
|
||||
const output = {};
|
||||
|
||||
input.forEach((version) => {
|
||||
const versionNumber = normalizeVersion(version.tag_name);
|
||||
const versionNumber = normalizeVersion(version.version);
|
||||
output[versionNumber] = version;
|
||||
output[versionNumber].state = 'unknown';
|
||||
});
|
||||
|
||||
return output;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { app } from 'electron';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
const getConfigPath = () => {
|
||||
const userDataPath = app.getPath('userData');
|
||||
return path.join(userDataPath, 'FirstRun', 'electron-app-first-run');
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether or not the app is being run for
|
||||
* the first time
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isFirstRun(): boolean {
|
||||
const configPath = getConfigPath();
|
||||
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fs.outputFileSync(configPath, '');
|
||||
} catch (error) {
|
||||
console.warn(`First run: Unable to write firstRun file`, error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Sort of like the `classnames` module, but a bit simpler
|
||||
* (and therefore lighter).
|
||||
*
|
||||
* @param {...Array<any>} args
|
||||
* @returns {string}
|
||||
*/
|
||||
export function classNames(...args: Array<any>): string {
|
||||
const classes: Array<string> = [];
|
||||
|
||||
for (const arg of args) {
|
||||
if (!arg) continue;
|
||||
|
||||
if (typeof arg === 'string' || typeof arg === 'number') {
|
||||
classes.push(arg.toString());
|
||||
} else if (Array.isArray(arg) && arg.length) {
|
||||
const inner = classNames.apply(null, arg);
|
||||
|
||||
if (inner) {
|
||||
classes.push(inner);
|
||||
}
|
||||
} else if (typeof arg === 'object') {
|
||||
for (const key in arg) {
|
||||
if ({}.hasOwnProperty.call(arg, key) && arg[key]) {
|
||||
classes.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Returns the correct name of ELectron for the current platform
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getElectronNameForPlatform(): string {
|
||||
if (process.platform === 'win32') {
|
||||
return 'electron.exe';
|
||||
} else if (process.platform === 'darwin') {
|
||||
return 'Electron.app';
|
||||
} else {
|
||||
return 'electron';
|
||||
}
|
||||
}
|
||||
@@ -33,9 +33,9 @@ export async function exec(dir: string, cliArgs: string): Promise<string> {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function maybeFixPath(): Promise<void> {
|
||||
export async function maybeFixPath(): Promise<void> {
|
||||
if (!_fixPathCalled && process.platform !== 'win32') {
|
||||
const fixPaths = (await import('fix-path')).default;
|
||||
const fixPaths = require('fix-path');
|
||||
fixPaths();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@ export function getFocusedEditor(
|
||||
});
|
||||
|
||||
return focusedKey
|
||||
? window.ElectronFiddle.editors[focusedKey] || null
|
||||
? window.ElectronFiddle.editors[focusedKey]
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Hightlights part of a string
|
||||
*
|
||||
* Inspired by https://github.com/palantir/blueprint/blob/develop/packages/docs-app/src/examples/select-examples/films.tsx
|
||||
* License: https://github.com/palantir/blueprint/blob/develop/LICENSE
|
||||
* Copyright 2017 Palantir Technologies, Inc. All rights reserved.
|
||||
*
|
||||
* @export
|
||||
* @param {string} text
|
||||
* @param {string} query
|
||||
* @returns
|
||||
*/
|
||||
export function highlightText(text: string, query: string): Array<React.ReactNode | string> | null {
|
||||
let lastIndex = 0;
|
||||
|
||||
const words = query
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 0)
|
||||
.map((s) => s.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'));
|
||||
|
||||
if (words.length === 0) return [ text ];
|
||||
|
||||
const regexp = new RegExp(words.join('|'), 'gi');
|
||||
const tokens: Array<React.ReactNode> = [];
|
||||
|
||||
while (true) {
|
||||
const match = regexp.exec(text);
|
||||
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
|
||||
const length = match[0].length;
|
||||
const before = text.slice(lastIndex, regexp.lastIndex - length);
|
||||
|
||||
if (before.length > 0) {
|
||||
tokens.push(before);
|
||||
}
|
||||
|
||||
lastIndex = regexp.lastIndex;
|
||||
tokens.push(<strong key={lastIndex}>{match[0]}</strong>);
|
||||
}
|
||||
|
||||
const rest = text.slice(lastIndex);
|
||||
|
||||
if (rest.length > 0) {
|
||||
tokens.push(rest);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Returns the value of an object's property at a given path.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {*} obj
|
||||
* @returns {any}
|
||||
*/
|
||||
export function getAtPath(input: string, obj: any): any {
|
||||
return input
|
||||
.split('.')
|
||||
.reduce((o, s) => o[s], obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value of a property of an object at a given path
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {*} obj
|
||||
* @param {*} val
|
||||
*/
|
||||
export function setAtPath(input: string, obj: any, val: any) {
|
||||
const pathValues = input.split('.');
|
||||
|
||||
pathValues.reduce((o, s, i) => {
|
||||
if (i !== pathValues.length - 1) {
|
||||
return o[s];
|
||||
}
|
||||
|
||||
o[s] = val;
|
||||
}, obj);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeVersion(version: string): string {
|
||||
export function normalizeVersion(version: string = ''): string {
|
||||
if (version.startsWith('v')) {
|
||||
return version.slice(1);
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,20 @@ export function sortedElectronMap<T>(
|
||||
mapFn: (key: string, version: ElectronVersion) => T
|
||||
) {
|
||||
return Object.keys(versions)
|
||||
.sort((a, b) => semver.gt(a, b, true) ? -1 : 1)
|
||||
.sort((a, b) => {
|
||||
if (!semver.valid(a) && !semver.valid(b)) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
if (!semver.valid(a)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!semver.valid(b)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return semver.gt(a, b, true) ? -1 : 1;
|
||||
})
|
||||
.map((key) => mapFn(key, versions[key])) as Array<T>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Returns the opposite of a Monaco-style boolean.
|
||||
*
|
||||
* @param {(boolean | string)} input
|
||||
* @returns {(boolean | string)}
|
||||
*/
|
||||
export function toggleMonaco(input: boolean | string): boolean | string {
|
||||
if (input === 'off') return 'on';
|
||||
if (input === 'on') return 'off';
|
||||
|
||||
return !input;
|
||||
}
|
||||