Compare commits

...
250 Commits
Author SHA1 Message Date
Felix Rieseberg 7a223fd3dc fix: Allow contributors.js to use GitHub credentials 2019-02-01 15:14:41 -08:00
Felix Rieseberg 70d4902b87 chore: Bump version to 0.5.0 2019-01-22 17:37:12 -08:00
Felix Rieseberg a93a99e3f0 Merge pull request #165 from electron/electron-4
feat: Update to Electron 4
2019-01-22 17:32:10 -08:00
Felix Rieseberg 4c209159e6 chore: jk, 4.0.2 2019-01-22 17:21:34 -08:00
Felix Rieseberg b09a248c96 feat: Update to Electron 4 2019-01-22 17:04:29 -08:00
Felix Rieseberg 6f09ed9c2a Merge pull request #163 from electron/forge-6-b32
chore: update Electron Forge to 6.0.0-beta.32
2019-01-21 12:51:56 -08:00
Felix Rieseberg c1ae0ecd86 Merge pull request #162 from electron/miniak/node-integration
feat: support upcoming Electron 5.0 with nodeIntegration disabled by default
2019-01-21 12:51:17 -08:00
Mark Lee 1e9a81fcf4 chore: update Electron Forge to 6.0.0-beta.32 2019-01-16 16:37:40 -08:00
Milan Burda 2061b26ed6 feat: support upcoming Electron 5.0 with nodeIntegration disabled by default 2019-01-16 20:10:52 +01:00
Felix Rieseberg 88ce4a76e9 Merge pull request #160 from bpasero/bpasero-run-f5
feat: Allow to run a fiddle via F5
2019-01-11 14:40:49 -08:00
Benjamin Pasero 9b4ef4c0d2 Allow to run a fiddle via F5
Enables to run a fiddle just with the keyboard.
2019-01-11 11:50:13 +01:00
Felix Rieseberg dca2cd67e5 Merge pull request #156 from electron/mac-better-frame
feat: support double click action on custom mac window title
2019-01-10 14:18:56 -08:00
Samuel Attard 6bab353088 fix: properly animate the opening / closing of the terminal 2019-01-09 13:44:10 +13:00
Samuel Attard e6cc44ca37 feat: support double click action on the window title bar on macOS 2019-01-09 13:43:55 +13:00
Felix Rieseberg d039c5dbf5 Merge pull request #155 from electron/bump-version-042
chore: Bump version to 0.4.2
2019-01-08 12:31:30 -08:00
Felix Rieseberg c155fff8b0 chore: Bump version to 0.4.2 2019-01-08 11:40:45 -08:00
Felix Rieseberg 11202a8d6b Merge pull request #152 from electron/100-coverage
test: Full unit test coverage
2019-01-08 11:38:52 -08:00
Felix Rieseberg 475127f5b1 fix: Travis coverage report 2019-01-08 11:17:13 -08:00
Felix Rieseberg feee5be2f8 test: Improve coverage, fix report 2019-01-08 11:11:56 -08:00
Felix Rieseberg 678c7f1578 chore: Fix coverage report 2019-01-08 10:54:59 -08:00
Felix Rieseberg d2fcb8071b test: Correctly report error 2019-01-07 23:18:57 -08:00
Felix Rieseberg 0ab35dc867 test: Update snapshot 2019-01-07 23:16:07 -08:00
Felix Rieseberg 377d221191 chore: Final cleanup 2019-01-07 23:09:42 -08:00
Felix Rieseberg cbdab9d662 test: Cover all of exec 2019-01-07 23:09:42 -08:00
Felix Rieseberg 6875ab392e test: Cover all of versions 2019-01-07 23:09:42 -08:00
Felix Rieseberg 58ec0f015c test: Cover all of file-manager 2019-01-07 23:09:42 -08:00
Felix Rieseberg 2aef0e776e test: Cover all of app 2019-01-07 23:09:42 -08:00
Felix Rieseberg b2a7d6b2a4 test: Cover all of tour 2019-01-07 23:09:42 -08:00
Felix Rieseberg 303d6bc79a test: Cover all of tour-welcome 2019-01-07 23:09:42 -08:00
Felix Rieseberg d028e7c915 test: Cover all of settings-general-appearance 2019-01-07 23:09:42 -08:00
Felix Rieseberg f04631b6c1 test: Linting, cleanup 2019-01-07 23:09:42 -08:00
Felix Rieseberg c1bb524b59 test: Cover all of settings-electron 2019-01-07 23:09:42 -08:00
Felix Rieseberg 4a38fda293 test: Cover all of editors 2019-01-07 23:09:42 -08:00
Felix Rieseberg b98ad2b572 test: Cover all of dialog-token 2019-01-07 23:09:42 -08:00
Felix Rieseberg 6118b94b09 test: Cover all of add-version 2019-01-07 23:09:42 -08:00
Felix Rieseberg d0c1ee1a82 test: Cover all of publish-button 2019-01-07 23:09:42 -08:00
Felix Rieseberg 525715a911 test: Cover all of address-bar 2019-01-07 23:09:42 -08:00
Felix Rieseberg afbc0c9e6c test: Cover squirrel 2019-01-07 23:09:42 -08:00
Felix Rieseberg c1a9cdf88b chore: Add linter 2019-01-07 23:09:42 -08:00
Felix Rieseberg d281e61b8a test: Cover shared-constants (lol) 2019-01-07 23:09:42 -08:00
Felix Rieseberg 2fbbc10497 test: Cover update 2019-01-07 23:09:42 -08:00
Felix Rieseberg e8226ca97d test: Cover all of protocol 2019-01-07 23:09:42 -08:00
Felix Rieseberg 08f2dbe937 test: Cover all of menu 2019-01-07 23:09:42 -08:00
Felix Rieseberg 68813edb51 test: Cover all of main 2019-01-07 23:09:42 -08:00
Felix Rieseberg a32261414b test: Cover all of ipcMainManager 2019-01-07 23:09:42 -08:00
Felix Rieseberg 42f67e7b21 test: Cover all of first-run 2019-01-07 23:09:41 -08:00
Felix Rieseberg 7d9b0ed79d fix: Use localecompare for invalid versions 2019-01-07 23:09:41 -08:00
Felix Rieseberg 47f1b5c4c3 test: Cover all of files 2019-01-07 23:09:41 -08:00
Felix Rieseberg 432322694d test: Correctly use "it" 2019-01-07 23:09:41 -08:00
Felix Rieseberg 889e76cbae test: Cover all of output 2019-01-07 23:09:41 -08:00
Felix Rieseberg 4a689e5c06 test: Cover all of settings-credtis 2019-01-07 23:09:41 -08:00
Felix Rieseberg 3113f324cc test: Cover all of npm 2019-01-07 23:09:41 -08:00
Felix Rieseberg 64addb1fb0 test: Cover all of state 2019-01-07 23:09:41 -08:00
Felix Rieseberg e6facf60c1 test: Cover all of runner & binary 2019-01-07 23:09:41 -08:00
Felix Rieseberg 00cb5cf6c6 Merge pull request #151 from electron/different-constants
fix: Don't require all of electron
2019-01-07 00:39:19 +08:00
Felix Rieseberg 3e8dbebe42 Merge pull request #150 from electron/update-dependencies
feat: Update Electron, Monaco, React, and others
2019-01-07 00:30:24 +08:00
Felix Rieseberg dcdef4aa4e fix: One more incorrect import 2019-01-07 00:25:25 +08:00
Felix Rieseberg 590389abdc fix: Update tests 2019-01-07 00:14:37 +08:00
Felix Rieseberg 18a8f0c142 fix: Deal with incorrect types 2019-01-07 00:10:44 +08:00
Felix Rieseberg a8e8ab6f04 fix: Don't require all of electron 2019-01-07 00:05:17 +08:00
Felix Rieseberg 45ed35467a feat: Update react-mosaic-component 2019-01-06 23:47:36 +08:00
Felix Rieseberg eb49cb296a feat: Update to Electron 3.0.14 2019-01-06 23:39:48 +08:00
Felix Rieseberg c2344d50d2 feat: Update dependencies 2019-01-06 23:33:32 +08:00
Felix Rieseberg 7786e113d9 Merge pull request #148 from electron/fix-defaultversion
fix: Ensure correct version selection & display
2019-01-06 22:58:05 +08:00
Felix Rieseberg f8b51870d8 Merge pull request #146 from electron/use-classnames
refactor: Use classnames module
2019-01-06 21:51:46 +08:00
Felix Rieseberg 601c28dcf8 fix: Handle EEXIST in unzipper 2019-01-06 21:47:25 +08:00
Felix Rieseberg 49379af856 feat: Display "checking status" in run button 2019-01-06 21:47:11 +08:00
Felix Rieseberg 8347f79756 fix: Correctly choose defaultVersion 2019-01-06 21:32:21 +08:00
Felix Rieseberg c102128d5a infra: Use classnames module 2019-01-06 21:01:42 +08:00
Felix Rieseberg fcff053136 Merge pull request #145 from electron/coverage-report
test: Add coverage reports
2019-01-06 20:27:06 +08:00
Felix Rieseberg c3641cfa04 test: Add coverage reports 2019-01-06 20:09:21 +08:00
Felix Rieseberg 6eba3cbdcc Merge pull request #143 from electron/more-tests-2
test: Write even more tests
2019-01-06 19:57:47 +08:00
Felix Rieseberg 6393c59d5c test: One more app spec 2019-01-04 23:56:53 +08:00
Felix Rieseberg 543e86bc91 chore: Fix linter issue 2019-01-04 20:38:34 +08:00
Felix Rieseberg ea5fdd426c test: Add an app setup() spec 2019-01-04 20:33:45 +08:00
Felix Rieseberg fecc499cc5 test: Add an exec test case 2019-01-04 20:13:07 +08:00
Felix Rieseberg 8d0a1524b9 test: Remove impossible case in focused-editor 2019-01-04 20:12:58 +08:00
Felix Rieseberg 918e91965a test: Add a version spec 2019-01-04 19:59:38 +08:00
Felix Rieseberg 15bab9ff86 chore: Fix linter issues 2019-01-04 19:49:37 +08:00
Felix Rieseberg 7d879b59cf test: Add runner specs 2019-01-04 19:42:33 +08:00
Felix Rieseberg 72f7e058d6 test: Add some state tests 2019-01-04 19:26:07 +08:00
Felix Rieseberg a65e044b5c test: Add some file manager tests 2019-01-04 17:13:41 +08:00
Felix Rieseberg a4c4590816 test: Remove obselete snapshot 2019-01-04 16:58:14 +08:00
Felix Rieseberg 6fbb816be3 test: Add MockState 2019-01-04 16:56:32 +08:00
Felix Rieseberg ae0ba026f2 test: More setting tests 2019-01-04 16:56:27 +08:00
Felix Rieseberg cc2a44d10a test: More app tests 2019-01-04 16:56:03 +08:00
Felix Rieseberg 3e6eb16974 Merge pull request #142 from electron/more-tests
test: More test, more minor refactoring
2019-01-02 22:47:52 +08:00
Felix Rieseberg 4212284fc4 test: Update Electron mock 2019-01-02 21:54:46 +08:00
Felix Rieseberg 8b35ff8f95 test: Add octokit spec 2019-01-02 21:54:39 +08:00
Felix Rieseberg 4d00ee9fd4 test: Add files spec 2019-01-02 21:54:27 +08:00
Felix Rieseberg 590db5304d test: Cover sorted map better 2019-01-02 21:54:18 +08:00
Felix Rieseberg e519ce2f13 test: Add highlight spec 2019-01-02 21:54:09 +08:00
Felix Rieseberg 8221920fab test: Add main spec 2019-01-02 21:53:48 +08:00
Felix Rieseberg 5de7ed7d9d test: Move out first-run, add tests 2019-01-02 21:53:36 +08:00
Felix Rieseberg a1586e864a Merge pull request #141 from electron/tests-and-security
test: Increase test coverage, make some code safer
2019-01-02 09:31:36 +08:00
Felix Rieseberg 60745ff8bb test: Add fancy-import test 2018-12-30 19:09:28 +01:00
Felix Rieseberg 4ac20aa5c2 test: Add highlight-text test 2018-12-30 19:09:20 +01:00
Felix Rieseberg 48faec9741 test: Add tests for firstRun, use outputFile 2018-12-30 18:51:08 +01:00
Felix Rieseberg c0f4e95a67 test: Add some version tests 2018-12-30 18:39:39 +01:00
Felix Rieseberg d5c8c8bd96 test: Add some editors tests 2018-12-30 18:18:39 +01:00
Felix Rieseberg 80ff396879 test: Add dialog-warning tests 2018-12-30 17:34:35 +01:00
Felix Rieseberg bf42abc88d test: Update dialog-token tests 2018-12-30 17:11:49 +01:00
Felix Rieseberg eaa064ea8d test: Update dialog-add-version tests 2018-12-30 17:01:57 +01:00
Felix Rieseberg e804436669 test: Update commands-runner tests 2018-12-30 16:58:30 +01:00
Felix Rieseberg 08d07b34a4 chore: Safer commands-runner 2018-12-30 16:53:42 +01:00
Felix Rieseberg 74ba7db13c Merge pull request #140 from electron/fix-cert-file
ci: Properly find Windows codesign certificate
2018-12-24 18:06:42 +01:00
Felix Rieseberg 0fced3aefd ci: Resolve path correctly 2018-12-24 17:56:41 +01:00
Felix Rieseberg 13c9217f9f ci: Fix variable test 2018-12-24 17:52:31 +01:00
Felix Rieseberg 346742b242 ci: Properly find Windows codesign certificate 2018-12-24 17:21:15 +01:00
Felix Rieseberg aefe273516 Merge pull request #139 from electron/bump-041
chore: Bump version number
2018-12-24 16:20:59 +01:00
Felix Rieseberg 337d19c33a chore: Bump version number 2018-12-24 16:15:58 +01:00
Felix Rieseberg ebc6f91dea Merge pull request #138 from electron/fix-tour
fix: The Tour
2018-12-24 16:09:50 +01:00
Felix Rieseberg 25446aa9e4 chore: Update snapshots 2018-12-24 15:27:29 +01:00
Felix Rieseberg f2859c053b chore: Goodbye, dialog component 2018-12-24 14:39:56 +01:00
Felix Rieseberg 49cd192817 feat: Modernize and improve tour 2018-12-24 14:26:09 +01:00
Felix Rieseberg 9bdd09c6b5 fix: Tour selectors 2018-12-24 13:48:13 +01:00
Felix Rieseberg c3d36b29b0 feat: Modernize the tour buttons 2018-12-24 13:44:15 +01:00
Felix Rieseberg 1b69a2ff52 Merge pull request #137 from electron/ci-publish-win
ci: Allow AppVeyor to codesign
2018-12-24 12:19:18 +00:00
Felix Rieseberg 311497d080 ci: AppVeyor: Use Node 10 2018-12-24 13:01:13 +01:00
Felix Rieseberg 55b7739d36 ci: AppVeyor: Upload build artifacts, build ia32 2018-12-24 13:00:28 +01:00
Felix Rieseberg 0fda97eb2f ci: AppVeyor: Print build artifacts 2018-12-24 12:52:45 +01:00
Felix Rieseberg 879815fc02 ci: Allow AppVeyor to codesign 2018-12-24 12:39:33 +01:00
Felix Rieseberg c469c3e856 Merge pull request #136 from electron/fix-windows-tests
test: Fix Windows tests
2018-12-24 08:29:31 +00:00
Felix Rieseberg 1b6750e0c5 Merge pull request #135 from electron/ci-publish
ci: Allow Travis to create signed macOS builds
2018-12-24 08:23:41 +00:00
Felix Rieseberg f14bd5c041 test: Fix Windows tests 2018-12-24 09:22:15 +01:00
Felix Rieseberg 76115a3e9c fix: Little linter error 2018-12-24 09:13:43 +01:00
Felix Rieseberg 65f86e0177 Merge branch 'ci-publish' of https://github.com/electron/fiddle into ci-publish 2018-12-24 09:10:36 +01:00
Felix Rieseberg 18cacb1532 chore: Silence the import 2018-12-24 09:10:27 +01:00
Mark Leeandfelixrieseberg 6a924ac88b Update forge.config.js
Co-Authored-By: felixrieseberg <felix@felixrieseberg.com>
2018-12-24 07:56:55 +00:00
Felix Rieseberg d125af6775 chore: Update the CI config 2018-12-24 08:51:15 +01:00
Felix Rieseberg 1a9a0579a5 chore: Add intermediate macOS certificates 2018-12-24 07:21:30 +01:00
Felix Rieseberg acee131288 chore: Install publisher-github 2018-12-24 07:20:45 +01:00
Felix Rieseberg 66536a5260 Merge pull request #134 from electron/fix-data-dir
feat: Prune appData directories after runs
2018-12-24 05:27:42 +00:00
Shelley Vohr 7941a83109 chore: fix 8 low severity vulns 2018-12-23 10:26:57 -07:00
Felix Rieseberg b312b80b69 fix: Throw error in contributors.js 2018-12-23 18:26:22 +01:00
Shelley Vohr b3c3b73b7d feat: prompt users to move to application folder (#132) 2018-12-23 10:25:37 -07:00
Shelley Vohr 0a8e9cde1b chore: add codebytere to contributors 2018-12-23 10:21:55 -07:00
Felix Rieseberg ca3f48697d chore: Add some tests 2018-12-23 17:57:51 +01:00
Felix Rieseberg 067e5cd5e2 feat: Delete app data dir after run 2018-12-23 17:57:51 +01:00
Felix Rieseberg 7e16c8c1f2 chore: Move runner logic out of component 2018-12-23 17:57:51 +01:00
Felix Rieseberg e932aad47b Merge pull request #131 from electron/electron-311
feat: Update to Electron 3.0.11
2018-12-20 09:45:49 -08:00
Felix Rieseberg 82fb8290c3 Merge pull request #130 from electron/modern-add-version-dialog
feat: Modernize 'add local build' dialog
2018-12-20 09:38:29 -08:00
Felix Rieseberg 2eb8bb3cf5 infra: Update to Electron 3.0.11 2018-12-20 09:38:04 -08:00
Felix Rieseberg 3d6448030a feat: Modernize 'add local build' dialog 2018-12-20 09:28:50 -08:00
Felix Rieseberg 65855908d1 Merge pull request #128 from electron/fix-confusing-dialog
fix: Confusing "Fiddle unsaved" dialog
2018-12-20 09:11:08 -08:00
Felix Rieseberg 9e986e24e2 Merge pull request #129 from electron/bump-040
chore: Update version number
2018-12-20 08:57:38 -08:00
Felix Rieseberg 9abc358bd9 fix: Linter issues 2018-12-20 08:57:23 -08:00
Felix Rieseberg 7f7785c69c chore: Update version number 2018-12-19 17:17:40 -08:00
Felix Rieseberg f36f300feb feat: Improve exit dialog, too 2018-12-19 17:16:59 -08:00
Felix Rieseberg 2c49214a25 fix: Confusing unsaved dialog 2018-12-19 16:57:52 -08:00
Felix Rieseberg c7a8163990 Merge pull request #127 from electron/fix-show-me
fix: "Show me" casing issue on Windows
2018-12-19 11:03:03 -08:00
Felix Rieseberg 97995b0ec2 fix: Lowercase for all templates 2018-12-19 10:24:48 -08:00
Felix Rieseberg d2377dcc30 Merge pull request #126 from electron/feat-npm-releases
feat: Use npm registry to fetch releases
2018-12-18 14:39:41 -08:00
Felix Rieseberg ac1197faaa fix: Two more fixes 2018-12-18 13:59:43 -08:00
Felix Rieseberg 9fb9aa6c92 fix: Make linter happy 2018-12-18 13:44:53 -08:00
Felix Rieseberg 6c5e41a2f1 feat: Use npm to download releases 2018-12-18 12:32:13 -08:00
Felix Rieseberg 4b52c81101 chore: Update the built-in releases file 2018-12-18 12:31:47 -08:00
Felix Rieseberg 35f4f1293c infra: Have fetch-releases use npm 2018-12-18 12:31:47 -08:00
Felix Rieseberg 8840b1ef7f Merge pull request #125 from electron/forge-arch-squirrel
chore: automatically add arch to squirrel installer filename
2018-12-18 12:31:16 -08:00
Felix Rieseberg d516522af2 Merge pull request #124 from electron/feat-public-private
feat: Publish fiddles as public/private
2018-12-18 12:30:35 -08:00
Mark Lee 0d0a51a900 infra: automatically add arch to squirrel installer filename 2018-12-17 23:53:46 -08:00
Felix Rieseberg be3b1e4a5b feat: Publish fiddles as public/private 2018-12-17 16:42:55 -08:00
Felix Rieseberg 2acc7112b7 fix: gistId in address bar without url 2018-12-17 16:04:13 -08:00
Felix Rieseberg 407bf3caeb Merge pull request #120 from electron/redesign
feat: Minor redesign, new component library
2018-12-17 15:54:04 -08:00
Felix Rieseberg 49123116d5 fix: Incorrect "arch" reference 2018-12-17 14:08:57 -08:00
Felix Rieseberg 9f6bc2f26f fix: Remove FontAwesome 2018-12-17 09:04:20 -08:00
Felix Rieseberg 5585e2d5d0 feat: Modernize the Token Dialog 2018-12-13 16:37:51 -08:00
Felix Rieseberg 4235969526 fix: forge.config.js 2018-12-13 16:20:25 -08:00
Felix Rieseberg 659d41f1c8 feat: Modernize Electron settings, credits 2018-12-12 18:23:59 -08:00
Felix Rieseberg 2549992782 feat: Modernize settings menu items 2018-12-12 18:23:59 -08:00
Felix Rieseberg b7314b83ee feat: Modernize the GitHub settings 2018-12-12 18:23:59 -08:00
Felix Rieseberg 39c2a9164a feat: Modernize appearance settings 2018-12-12 18:23:59 -08:00
Felix Rieseberg 1a9dc1fbf7 chore: Reorganize some more files 2018-12-12 18:23:59 -08:00
Felix Rieseberg 0eb01c4692 feat: Modernize the publish button 2018-12-12 18:23:59 -08:00
Felix Rieseberg 96667a3b04 chore: Rename publish-button to commands-publish-button 2018-12-12 18:23:59 -08:00
Felix Rieseberg 4365513f57 feat: Modernize address bar 2018-12-12 18:23:59 -08:00
Felix Rieseberg 913b37c285 chore: Rename address-bar 2018-12-12 18:23:59 -08:00
Felix Rieseberg 66c55d39c0 fix: Correctly display colors 2018-12-12 18:23:59 -08:00
Felix Rieseberg 0f3a3aca72 chore: Reorganize styles 2018-12-12 18:23:59 -08:00
Felix Rieseberg 3fc974a758 feat: Modernize the run button 2018-12-12 18:23:59 -08:00
Felix Rieseberg 72b805059c feat: Update version selector 2018-12-12 18:23:59 -08:00
Felix Rieseberg 291882ac5c infra: Simplify and improve parcel build 2018-12-12 18:23:59 -08:00
Felix Rieseberg 3c05cc8ec7 Merge pull request #119 from electron/update-windows-installer-icon
chore: update windows installer icon
2018-12-12 15:30:13 -08:00
Mark Lee 75d2f9ff72 🎨 update windows installer icon 2018-12-12 13:30:35 -08:00
Felix Rieseberg 0119f0ce69 Merge pull request #118 from electron/new-icon
docs: New icon
2018-12-12 13:22:48 -08:00
Mark Lee 8ed5ea94b5 Add SVG to .deb file 2018-12-11 23:53:04 -08:00
simurai deaac4e15c docs: Add icon as SVG 2018-12-12 12:47:53 +09:00
simurai 48e58c095d docs: Use transparent background for README icon 2018-12-11 16:48:07 +09:00
simurai 28f0eca429 docs: Replace loading gif 2018-12-11 12:02:02 +09:00
simurai 529906e1a9 docs: Replace icons 2018-12-11 09:20:15 +09:00
simurai 40c9aaeb7a docs: Add icon to README 2018-12-10 16:36:03 +09:00
Felix Rieseberg 15924c7741 fix: Make the linter happy 2018-11-27 15:47:56 -08:00
Felix Rieseberg c62041a000 Merge pull request #115 from electron/updates-2
chore: Updates for everyone
2018-11-27 15:46:50 -08:00
Felix Rieseberg ff3e931d14 fix: Apply lint fixes 2018-11-27 13:40:58 -08:00
Felix Rieseberg 7cc16b6fab infra: Update to electron-forge v6 beta 30 2018-11-27 13:40:08 -08:00
Felix Rieseberg 45e8e311da Merge pull request #111 from electron/tour-buttons
fix: friendlier button text for intro tour
2018-11-27 13:08:39 -08:00
Felix Rieseberg b09e792583 chore: Fix type errors in npm 2018-11-27 13:07:57 -08:00
Felix Rieseberg cdb862442c infra: Use library mode in fontawesome 2018-11-27 13:03:19 -08:00
Felix Rieseberg 1e1566dbdb infra: Update TypeScript 2018-11-27 13:03:04 -08:00
Felix Rieseberg e571fcca4f fix: style and logic in npm.ts 2018-11-27 13:02:50 -08:00
Felix Rieseberg e61e49987e infra: Update node-abi, node-fetch 2018-11-27 11:47:45 -08:00
Felix Rieseberg 133411c150 infra: Update mobx 2018-11-27 11:46:48 -08:00
Felix Rieseberg db8c7c32ab infra: Update react, semver 2018-11-27 11:44:59 -08:00
Felix Rieseberg 003e27f57e infra: Update monaco-editor 2018-11-27 11:41:57 -08:00
Felix Rieseberg 72d717bd48 infra: Update Electron to 3.0.10 2018-11-26 15:11:34 -08:00
Felix Rieseberg 675500fe25 infra: Update fontawesome 2018-11-26 12:38:23 -08:00
Felix Rieseberg fbe2311d53 infra: Update electron-forge to lastest beta 2018-11-26 12:37:09 -08:00
Felix Rieseberg ef42b1affe tests: Fix tests 2018-11-23 11:35:53 -08:00
Felix Rieseberg 2adde7d9ce Merge pull request #114 from ada-lovecraft/master
fix: erroneous require behavior
2018-11-23 11:28:54 -08:00
Ada Lovecraft 5fa0ddcc73 cleaned up npm module searching and added inline docs 2018-11-21 12:22:46 -06:00
Ada Lovecraft 5d501f0827 fixed npm module loading 2018-11-21 00:33:35 -06:00
Jeremy Apthorp d738936952 friendlier button text for intro tour
In general I like it when buttons are labeled with what's going to happen when I click on them
2018-11-09 13:33:01 -08:00
Shelley Vohr 57ce8b0b79 chore: remove unused requires in main (#109) 2018-10-30 16:55:08 -07:00
Felix Rieseberg 46f4efb4b4 Merge pull request #106 from electron/minor-fixups
feat: Add key property for buttons, update electron and typescript
2018-10-11 16:35:51 -04:00
HashimotoYT 10a696a9d7 Update test snapshot 2018-10-11 23:25:38 +03:00
HashimotoYT 585c0e1183 Update Electron, add myself to contributors. 2018-10-11 21:40:16 +03:00
HashimotoYT 946d32fdd0 Update TypeScript and Electron 2018-10-11 02:19:17 +03:00
HashimotoYT f09363ec26 Add key prop for buttons in token dialog 2018-10-11 02:11:40 +03:00
Felix Rieseberg 1a6876497a Merge pull request #104 from electron/hashi/gist-error-message
feat: provide error message for Token dialog
2018-10-08 16:49:56 -07:00
HashimotoYT 6bc04f5d2a Trying revert templates 2018-10-09 02:16:46 +03:00
HashimotoYT 9f7bf9d299 Provide error message for token dialog 2018-10-08 01:33:09 +03:00
Felix Rieseberg 3b7873917f Merge pull request #103 from electron/local-versions
feat: Add support for local Electron builds (with versions)
2018-10-05 19:17:04 -07:00
Felix Rieseberg 4616f12d6c 👷 Fix snapshot issues 2018-10-05 19:02:57 -07:00
Felix Rieseberg 63f37c9553 👷 Add a few tests 2018-10-05 18:56:22 -07:00
Felix Rieseberg 09edfa16b0 👷 Update snapshots 2018-10-05 18:56:22 -07:00
Felix Rieseberg da3a607bc1 🔧 Pretty version chooser 2018-10-05 18:56:22 -07:00
Felix Rieseberg 5fcc34b035 🔧 Parse the version 2018-10-05 18:56:22 -07:00
Felix Rieseberg dfed297226 👷 Fix existing tests 2018-10-05 18:56:22 -07:00
Felix Rieseberg 7cb2c4d188 🔧 Run local versions 2018-10-05 18:56:22 -07:00
Felix Rieseberg f0215d8c90 🔧 Add local version dialog 2018-10-05 18:56:21 -07:00
Felix Rieseberg ffdb57e2f3 🔧 More of it all 2018-10-05 18:56:11 -07:00
Felix Rieseberg e5dd6ffb4e 🎨 Use the correct delete icon 2018-09-28 16:11:32 -07:00
Felix Rieseberg 1083b6c2de 🔧 Cleanup 2018-09-28 13:45:46 -07:00
Felix Rieseberg 73b6cb0cb2 🔧 Get all versions during refresh 2018-09-28 13:14:53 -07:00
Felix Rieseberg 3e5914b192 🔧 Basic state support for local versions 2018-09-28 13:11:30 -07:00
Felix Rieseberg fe81d5c14c Merge pull request #102 from electron/update-dependencies
chore: Update all the dependencies
2018-09-27 13:16:49 -07:00
Felix Rieseberg c85536a8b9 🔧 Implement deprecations 2018-09-27 13:05:03 -07:00
Felix Rieseberg 8d90dbed09 🔧 Update tests 2018-09-27 13:01:44 -07:00
Felix Rieseberg b918c13c72 🔧 Fix standardJs lint issues 2018-09-27 11:43:47 -07:00
Felix Rieseberg 570bab9c29 🔧 Refactor and fix code lint issues 2018-09-27 11:42:50 -07:00
Felix Rieseberg f0f7ed22ee 📦 Update stylelint and standard 2018-09-27 10:40:43 -07:00
Felix Rieseberg a62c1550a1 📦 Update TypeScript 2018-09-27 10:39:39 -07:00
Felix Rieseberg 754473e10a 📦 Update to Electron 3.0.1 2018-09-27 10:32:21 -07:00
Felix Rieseberg 732f81cd33 📦 Update namor 2018-09-27 10:20:01 -07:00
Felix Rieseberg 47e5182117 📦 Update Octokit 2018-09-27 10:18:55 -07:00
Felix Rieseberg 92c831cc7b 📦 Update FontAwesome 2018-09-27 10:18:04 -07:00
Felix Rieseberg 8ffb215797 📦 Bump React 2018-09-27 10:17:30 -07:00
Felix Rieseberg 616264f017 📦 Bump mobx 2018-09-27 10:16:21 -07:00
Felix Rieseberg ee6e474b04 📦 Bump the version number 2018-09-27 10:15:46 -07:00
Felix Rieseberg ee8fa678f9 Merge pull request #101 from electron/fix-download-link
fix: don't pin download link to 0.1 tag
2018-09-27 09:44:25 -07:00
243 changed files with 12967 additions and 7736 deletions
+33 -2
View File
@@ -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
+3 -3
View File
@@ -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
[![Build Status](https://travis-ci.org/electron/fiddle.svg?branch=master)](https://travis-ci.org/electron/fiddle)
[![Build Status](https://travis-ci.org/electron/fiddle.svg?branch=master)](https://travis-ci.org/electron/fiddle) [![Coverage Status](https://coveralls.io/repos/github/electron/fiddle/badge.svg?branch=master)](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>
![Screenshot](https://user-images.githubusercontent.com/1426799/43873471-8893e3a8-9b3b-11e8-975b-e357f8039b5c.png)
<img src="https://user-images.githubusercontent.com/378023/49716908-7d2b0500-fc98-11e8-8f52-def504802509.png" width="880px" alt="Electron Fiddle screenshot">
# Features
+37
View File
@@ -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
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 KiB

After

Width:  |  Height:  |  Size: 140 KiB

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="SVG" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icon">
<circle id="BG" stroke="#FFFFFF" stroke-width="36" fill="#E79537" cx="512" cy="512" r="430"></circle>
<g id="Glyph" transform="translate(336.000000, 280.000000)">
<path d="M92,335.02442 L92,44 C92,30.745166 102.745166,20 116,20 L356,20 C369.254834,20 380,30.745166 380,44 C380,57.254834 369.254834,68 356,68 L140,68 L140,188 L239.428571,188 C250.789858,188 260,198.745166 260,212 C260,225.254834 250.789858,236 239.428571,236 L140,236 L140,335.02442 C181.405453,345.681447 212,383.267854 212,428 C212,481.019336 169.019336,524 116,524 C62.980664,524 20,481.019336 20,428 C20,383.267854 50.5945469,345.681447 92,335.02442 Z" id="Shadow" fill="#000000" fill-rule="nonzero" opacity="0.0980980283"></path>
<path d="M72,315.02442 L72,24 C72,10.745166 82.745166,0 96,0 L336,0 C349.254834,0 360,10.745166 360,24 C360,37.254834 349.254834,48 336,48 L120,48 L120,168 L219.428571,168 C230.789858,168 240,178.745166 240,192 C240,205.254834 230.789858,216 219.428571,216 L120,216 L120,315.02442 C161.405453,325.681447 192,363.267854 192,408 C192,461.019336 149.019336,504 96,504 C42.980664,504 0,461.019336 0,408 C0,363.267854 30.5945469,325.681447 72,315.02442 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
<path d="M96,456 C122.509668,456 144,434.509668 144,408 C144,381.490332 122.509668,360 96,360 C69.490332,360 48,381.490332 48,408 C48,434.509668 69.490332,456 96,456 Z" id="Dot" fill="#EB5E00"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+37 -17
View File
@@ -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
}
}
]
};
}
+4 -2
View File
@@ -20,11 +20,13 @@
],
"coverageReporters": [
"json",
"html"
"html",
"lcov"
],
"collectCoverageFrom": [
"src/**/*.{ts,tsx}",
"!**/*.d.ts"
"!**/*.d.ts",
"!**/*constants.ts"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
+4187 -3278
View File
File diff suppressed because it is too large Load Diff
+48 -45
View File
@@ -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"
}
}
+2
View File
@@ -0,0 +1,2 @@
declare module 'fix-path';
declare module 'namor';
-14
View File
@@ -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');
+7 -3
View File
@@ -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')
+21 -14
View File
@@ -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;
+4 -6
View File
@@ -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
];
+32
View File
@@ -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;
}
}
}
-103
View File
@@ -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;
}
+66
View File
@@ -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;
}
-74
View File
@@ -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;
}
}
}
}
-38
View File
@@ -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";
-7
View File
@@ -1,7 +0,0 @@
@import "../variables.less";
hr {
border: 0;
border-top: @border;
margin: 2rem 0;
}
-58
View File
@@ -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;
}
-5
View File
@@ -1,5 +0,0 @@
@import "../variables.less";
a {
color: @foreground-2;
}
-26
View File
@@ -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;
}
-20
View File
@@ -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
}
}
-57
View File
@@ -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;
}
+5 -13
View File
@@ -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);
}
-33
View File
@@ -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;
}
}
}
+12 -13
View File
@@ -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";
+1
View File
@@ -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);
+4 -2
View File
@@ -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();
});
}
/**
+34
View File
@@ -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();
}
});
}
+43 -20
View File
@@ -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();
+3 -2
View File
@@ -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' ]),
});
}
+4 -2
View File
@@ -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}://`)) {
+3
View File
@@ -0,0 +1,3 @@
export function shouldQuit() {
return require('electron-squirrel-startup');
}
+3 -1
View File
@@ -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',
});
+4 -1
View File
@@ -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);
}
+33 -20
View File
@@ -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();
+28 -10
View File
@@ -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;
-20
View File
@@ -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
+16 -1
View File
@@ -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>
);
}
}
+17 -11
View File
@@ -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>
);
}
}
-111
View File
@@ -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;
}
}
+9 -2
View File
@@ -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>
);
}
+4 -56
View File
@@ -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);
}
+59 -11
View File
@@ -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 });
}
+1
View File
@@ -6,6 +6,7 @@ import { AppState } from '../state';
export interface CommandsProps {
appState: AppState;
// Used to keep testing conform
renderTimestamp?: (ts: number) => string;
}
+45 -16
View File
@@ -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);
}
}
}
+151 -149
View File
@@ -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;
}
}
+2 -1
View File
@@ -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>
);
+35 -6
View File
@@ -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';
}
}
+79 -61
View File
@@ -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>&lt;script /&gt;</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>&lt;script /&gt;</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 {
+50 -15
View File
@@ -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>
);
}
}
+5
View File
@@ -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');
+3 -4
View File
@@ -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;
+16 -9
View File
@@ -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) {
+57 -25
View File
@@ -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);
}
}
+141 -46
View File
@@ -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;
}
}
+3 -2
View File
@@ -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 '';
}
};
+6
View File
@@ -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 -2
View File
@@ -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 -1
View File
@@ -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
+216 -61
View File
@@ -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;
}
+4
View File
@@ -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';
+16
View File
@@ -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);
}
+4 -5
View File
@@ -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;
+30
View File
@@ -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;
}
-32
View File
@@ -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(' ');
}
+14
View File
@@ -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';
}
}
+2 -2
View File
@@ -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();
}
+1 -1
View File
@@ -14,6 +14,6 @@ export function getFocusedEditor(
});
return focusedKey
? window.ElectronFiddle.editors[focusedKey] || null
? window.ElectronFiddle.editors[focusedKey]
: null;
}
+53
View File
@@ -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;
}
+31
View File
@@ -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);
}
+1 -1
View File
@@ -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 -1
View File
@@ -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>;
}
+12
View File
@@ -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;
}
+1 -350
View File
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More