diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 87bf046f..32dc85c1 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,5 @@ + + **What version of WebTorrent Desktop?** (See the 'About WebTorrent' menu) **What operating system and version?** diff --git a/.gitignore b/.gitignore index 521c7d0f..f4e6e43c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules/ build/ dist/ -npm-debug.log.* +npm-debug.log* diff --git a/.nodemonignore b/.nodemonignore deleted file mode 100644 index edd9d60a..00000000 --- a/.nodemonignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -dist/ diff --git a/.travis.yml b/.travis.yml index 93c35a8e..33f6e7bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: node_js node_js: - 'node' -install: npm install standard +install: npm install standard depcheck walk-sync diff --git a/AUTHORS.md b/AUTHORS.md index 5385b294..bceeb904 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -28,5 +28,14 @@ - Rémi Jouannet (remijouannet@gmail.com) - Andrea Tupini (tupini07@gmail.com) - grunjol (grunjol@gmail.com) +- Jason Kurian (jasonk92@gmail.com) +- Vamsi Krishna Avula (vamsi_ism@outlook.com) +- Noam Okman (noamokman@gmail.com) +- PurgingPanda (t3ch0wn3r@gmail.com) +- Kai Curtis (morecode@kcurtis.com) +- Omri Litov (omrilitov@gmail.com) +- Alexey Romanov (romanalexey@gmail.com) +- Karan Thakkar (karanjthakkar@gmail.com) +- Nuno Campos (nuno.campos@me.com) #### Generated by bin/update-authors.sh. diff --git a/CHANGELOG.md b/CHANGELOG.md index 793264ee..16ff1413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,139 @@ # WebTorrent Desktop Version History +## v0.18.0 + +### Added +- Add a new "Transfers" menu for pausing or resuming all torrents (#1027) + +### Changed +- Update Electron to 1.4.15 + - Windows 32-bit: App can use 4GB of memory instead of just 2GB + - Fix "Portable App" writing crash reports to "%APPDATA%\Temp" (Windows) +- Updated WebTorrent engine to 0.98.5 + - Fix issue where http web seeds would sometimes stall + - Don't send 'completed' event to tracker again if torrent is already complete + - Add more peer ID entropy + - Set user-agent header for tracker http requests + +### Fixed +- Fix paste shortcut in tracker list on Create Torrent page (#1112) +- Auto-focus the 'OK' button in modal dialogs (#1058) +- Fix formatting issue in the speed stats on the Player page (#1039) + +## v0.17.2 - 2016-10-10 + +### Fixed +- Windows: Fix impossible-to-delete "Wired CD" default torrent +- Throttle browser-window 'move' and 'resize' events +- Fix crash ("Cannot read property 'files' of null" error) +- Fix crash ("TypeError: Cannot read property 'startPiece' of undefined") + +## v0.17.1 - 2016-10-03 + +### Changed +- Faster startup (improved by ~25%) +- Update Electron to 1.4.2 +- Remove support for pasting multiple newline-separated magnet links +- Reduce UX sound volume + +### Fixed +- Fix external player (VLC, etc.) opening before HTTP server was ready +- Windows (Portable App): Fix "Portable App" mode + - Write application support files to the "Portable Settings" folder + - Stop writing Electron "single instance" lock file to "%APPDATA%\Roaming\WebTorrent" + - Some temp data is still written to "%APPDATA%\Temp" (will be fixed in future version) +- Don't show pointer cursor on torrent list checkbox +- Trim extra whitespace from magnet links pasted into "Open Torrent Address" dialog +- Fix weird outline on 'Create Torrent' button + +## v0.17.0 - 2016-09-23 + +### Added +- Remember window size and position + +### Changed +- Torrent list redesign +- Quieter, more subtle sounds +- Got rid of the play button spinner, now goes to the player immediately +- Faster startup + +### Fixed +- Fix bug where playback rate could go negative +- Don't hide header when moused over player controls +- Fix Delete Data File on Windows +- Fix a sad, sad bug that resulted in 100+ MB config files +- Fix app DMG background image + +## v0.16.0 - 2016-09-18 + +### Added +- **Windows 64-bit support!** ([#931](https://github.com/feross/webtorrent-desktop/pull/931)) + - Existing 32-bit users will update to 64-bit automatically in next release + - 64-bit reduces likelihood of out-of-memory errors by increasing the address space + +### Fixed +- Mac: Fix background image on .DMG + +## v0.15.0 - 2016-09-16 + +### Added +- Option to start automatically on login +- Add integration tests +- Add more detailed telemetry to diagnose "buffer allocation failed" + +### Changed +- Disable playback controls while in external player (#909) + +### Fixed +- Fix several uncaught errors (#889, #891, #892) +- Update to the latest webtorrent.js, fixing some more uncaught errors +- Clicking on the "torrent finished" notification works again (#912) + +## v0.14.0 - 2016-09-03 + +### Added +- Autoplay through all files in a torrent (#871) +- Torrents now have a progress bar (#844) + +### Changed +- Modals now use Material UI +- Torrent list style improvements + +### Fixed +- Fix App.js crash in Linux (#882) +- Fix error on Windows caused by `setBadge` (#867) +- Don't crash when restarting after adding a magnet link (#869) +- Restore playback state when reopening player (#877) + +## v0.13.1 - 2016-08-31 + +### Fixed +- Fixed the Create Torrent page + +## v0.13.0 - 2016-08-31 + +### Added +- Support .m4a audio +- Better telemetry: log error versions, report more types of errors + +### Changed +- New look - Material UI. Rewrote Create Torrent and Preferences pages. + +### Fixed +- Fixed telemetry [object Object] and [object HTMLMediaElement] bugs +- Don't render player controls when playing externally, eg in VLC +- Don't play notification sounds during media playback + ## v0.12.0 - 2016-08-23 ### Added - - Custom external media player - Linux: add system-wide launcher and icons for Debian, including Ubuntu ### Changed - - Telemetry improvements: redact stacktraces, log app version ### Fixed - - Fix playback and download of default torrents ("missing path" error) (#804) - Fix Delete Torrent + Data for newly added magnet links - Fix jumpToTime error (#804) @@ -25,13 +146,11 @@ - Check for missing default download path and torrent folders on start up (#776) ### Changed - - Do not automatically set WebTorrent as the default handler for torrents (#771) - Torrents can only be created from the home screen (#770) - Update Electron to 1.3.3 (#772) ### Fixed - - Allow modifying the default tracker list on the Create Torrent page (#775) - Prevent opening multiple stacked Preference windows or Create Torrent windows (#770) - Windows: Player window auto-resize does not match video aspect ratio (#565) @@ -40,13 +159,11 @@ ## v0.10.0 - 2016-08-05 ### Added - - Drag-and-drop magnet links (selected text) is now supported (#284) - Windows: Add "User Tasks" shortcuts to app icon in Start Menu (#114) - Linux: Show badge count for completed torrent downloads ### Changed - - Change WebTorrent Desktop peer ID prefix to 'WD' to distinguish from WebTorrent in the browser, 'WW' (#688) - Switch UI to React to improve UI rendering speed (#729) - The primary bottleneck was actually `hyperx`, not `virtual-dom`. @@ -63,7 +180,6 @@ - Location history abstraction released independently as [`location-history`](https://www.npmjs.com/package/location-history) ### Fixed - - When streaming to VLC, set VLC window title to torrent file name (#746) - Fix "Cannot read property 'numPiecesPresent' of undefined" exception (#695) - Fix rare case where config file could not be completely written (#733) @@ -71,35 +187,29 @@ ## v0.9.0 - 2016-07-20 ### Added - - Save selected subtitles - Ask for confirmation before deleting torrents - Support Debian Jessie ### Changed - - Only send telemetry in production - Clean up the code. Split main.js, refactor lots of things ### Fixed - - Fix state.playing.jumpToTime behavior - Remove torrent file and poster image when deleting a torrent ## v0.8.1 - 2016-06-24 ### Added - - New URI handler: stream-magnet ### Fixed - - DLNA crashing bug ## v0.8.0 - 2016-06-23 ### Added - - Cast menu: choose which Chromecast, Airplay, or DLNA device you want to use - Telemetry: send basic data, plus stats on how often the play button works - Make posters from jpeg files, not just jpg @@ -107,17 +217,14 @@ - Windows thumbnail bar with a play/pause button ### Changed - - Nicer modal styles ### Fixed - - Windows tray icon now stays in the right state ## v0.7.2 - 2016-06-02 ### Fixed - - Fix exception that affects users upgrading from v0.5.1 or older - Ensure `state.saved.prefs` configuration exists - Fix window title on "About WebTorrent" window @@ -125,23 +232,19 @@ ## v0.7.1 - 2016-06-02 ### Changed - - Change "Step Forward" keyboard shortcut to `Alt+Left` (Windows) - Change "Step Backward" keyboard shortcut to to `Alt+Right` (Windows) ### Fixed - - First time startup bug -- invalid torrent/poster paths ## v0.7.0 - 2016-06-02 ### Added - - Improved AirPlay support -- using the new [`airplayer`](https://www.npmjs.com/package/airplayer) package - Remember volume setting in player, for as long as the app is open ### Changed - - Add (+) button now also accepts non .torrent files and creates a torrent from those files - Show prompt text in title bar for open dialogs (OS X) @@ -151,7 +254,6 @@ - Fix crash reporter not working (Windows) ### Fixed - - Re-enable WebRTC (web peers)! (OS X, Windows) - Windows support was disabled in v0.6.1 to work around a bug in Electron - OS X support was disabled in v0.4.0 to work around a 100% CPU bug @@ -162,7 +264,6 @@ - Fix torrent loading message UI misalignment ### Known issues - - When upgrading to WebTorrent Desktop v0.7.0, some torrent metadata (file list, selected files, whether torrent is streamable) will be cleared. Just start the torrent to re-populate the metadata. @@ -170,7 +271,6 @@ ## v0.6.1 - 2016-05-26 ### Fixed - - Disable WebRTC to work around Electron crash (Windows) - Will be re-enabled in the next version of WebTorrent, which will be based on the next version of Electron, where the bug is fixed. @@ -182,7 +282,6 @@ ## v0.6.0 - 2016-05-24 ### Added - - Added Preferences page to set Download folder - Save video position, resume playback from saved position - Add additional video player keyboard shortcuts (#275) @@ -192,7 +291,6 @@ - Add announcement feature ### Changed - - Nicer player UI - Reduce startup jank, improve startup time (#568) - Cleanup unsupported codec detection (#569, #570) @@ -200,7 +298,6 @@ - Improve subtitle positioning (#551) ### Fixed - - Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567) - Fix bugs in LocationHistory - When player is active, and magnet link is pasted, go back to list @@ -211,23 +308,19 @@ ## v0.5.1 - 2016-05-18 ### Fixed - - Fix auto-updater (OS X, Windows). ## v0.5.0 - 2016-05-17 ### Added - - Select/deselect individual files to torrent. - Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu. - "Add Subtitle File..." menu item. ### Changed - - When manually adding subtitle track(s), always switch to the new track. ### Fixed - - Magnet links throw exception on app launch. (OS X) - Multi-file torrents would not seed in-place, were copied to Downloads folder. - Missing 'About WebTorrent' menu item. (Windows) @@ -236,7 +329,6 @@ ## v0.4.0 - 2016-05-13 ### Added - - Better Windows support! - Windows 32-bit build. - Windows Portable App build. @@ -259,7 +351,6 @@ - New default torrent on first launch: The WIRED CD. ### Changed - - Improve app startup time by 40%. - UI tweaks: Reduce font size, reduce torrent list item height. - Add Playback menu for playback-related functionality. @@ -270,7 +361,6 @@ - Remove "Add Fake Airplay/Chromecast" menu items. ### Fixed - - Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is temporary. (OS X) - When fullscreen, make controls use the full window. (OS X) @@ -295,24 +385,20 @@ to this release! ## v0.3.3 - 2016-04-07 ### Fixed - - App icon was incorrect (OS X) ## v0.3.2 - 2016-04-07 ### Added - - Register WebTorrent as default handler for magnet links (OS X) ### Changed - - Faster startup time (50ms) - Update Electron to 0.37.5 - Remove the white flash when loading pages and resizing the window - Fix crash when sending IPC messages ### Fixed - - Fix installation bugs with .deb file (Linux) - Pause audio reliably when closing the window - Enforce minimimum window size when resizing player (for audio-only .mov files, which are 0x0) @@ -320,17 +406,14 @@ to this release! ## v0.3.1 - 2016-04-06 ### Added - - Add crash reporter to torrent engine process ### Fixed - - Fix cast screen background: cover, don't tile ## v0.3.0 - 2016-04-06 ### Added - - **Ubuntu/Debian support!** (.deb installer) - **DLNA streaming support** - Add "File > Quit" menu item (Linux) @@ -338,14 +421,12 @@ to this release! - Crash reporting ### Changed - - On startup, do not re-verify files when timestamps are unchanged - Moved torrent engine to an independent process, for better UI performance - Removed media queries (UI resizing based on window width) - Improved Chromecast icon, when connected ### Fixed - - "Download Complete" notification shows consistently - Create new torrents and seed them without copying to temporary folder - Clicking the "Download Complete" notification will always activate app @@ -364,7 +445,6 @@ Thanks to @dcposch, @grunjol, and @feross for contributing to this release. ## v0.2.0 - 2016-03-29 ### Added - - Minimise to tray (Windows, Linux) - Show spinner and download speed when player is stalled waiting for data - Highlight window on drag-and-drop @@ -373,12 +453,10 @@ Thanks to @dcposch, @grunjol, and @feross for contributing to this release. Linux users need to download new versions manually. ### Changed - - Renamed WebTorrent.app to WebTorrent Desktop - Add Cosmos Laundromat as a default torrent ### Fixed - - Only capture media keys when player is active - Update WebTorrent to 0.88.1 for performance improvements - When seeding, do not proactively connect to new peers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 048ab258..48981832 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ standard guarded open source project. There are a few basic ground-rules for contributors: -1. **No `--force` pushes** or modifying the Git history in any way. +1. **No `--force` pushes to master** or modifying history in any way. Rebasing and force pushing your own PR branch is fine. 2. **Non-master branches** should be used for ongoing work. 3. **Significant modifications** like API changes should be subject to a **pull request** to solicit feedback from other contributors. @@ -74,6 +74,99 @@ By making a contribution to this project, I certify that: including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. +## Release Procedure + +### 1. Create a new version + +- Update `AUTHORS` + + ``` + npm run update-authors + ``` + + Commit if necessary. The commit message should be "authors". + +- Write the changelog + + You can use `git log --oneline ..HEAD` to get a list of changes. + + Summarize them concisely in `CHANGELOG.md`. The commit message should be "changelog". + +- Update the version + + ``` + npm version [major|minor|patch] + ``` + + This creates both a commit and a git tag. + +- Make a PR + + Once the PR is reviewed, merge it: + + ``` + git push origin :master + ``` + + This makes it so that the commit hash on master matches the commit hash of the version tag. + + Finally, run: + + ``` + git push --tags + ``` + +### 2. Create the release binaries + +- On a Mac: + + ``` + npm run package -- darwin --sign + npm run package -- linux --sign + ``` + +- On Windows, or in a Windows VM: + + ``` + npm run package -- win32 --sign + ``` + +- Then, upload the release binaries to Github: + + ``` + npm run gh-release + ``` + + Follow the URL to a newly created Github release page. Manually upload the binaries from + `webtorrent-desktop/dist/`. Open the previous release in another tab, and make sure that you + are uploading the same set of files, no more, no less. + +### 3. Test it + +**This is the most important part.** + + - Manually download the binaries for each platform from Github. + + **Do not use your locally built binaries.** Modern OSs treat executables differently if they've + been downloaded, even though the files are byte for byte identical. This ensures that the + codesigning worked and is valid. + +- Smoke test WebTorrent Desktop on each platform. + + See Smoke Tests below for details. Open DevTools + on Windows and Mac, and ensure that the auto updater is running. If the auto updater does not + run, users will successfully auto update to this new version, and then be stuck there forever. + +### 4. Ship it + +- Update the website + + Create a pull request in [webtorrent.io](https://github.com/feross/webtorrent.io). Update + `config.js`, updating the desktop app version. + + As soon as this PR is merged, Jenkins will automatically redeploy the WebTorrent website, and + hundreds of thousands of users around the world will start auto updating. **Merge with care.** + ## Smoke Tests Before a release, check that the following basic use cases work correctly: diff --git a/README.md b/README.md index 3142cc16..64b5923d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@


- WebTorrent + + WebTorrent +
WebTorrent Desktop
@@ -10,9 +12,15 @@

The streaming torrent app. For Mac, Windows, and Linux.

- Gitter - Travis - Release + + Gitter + + + Travis + + + Release +

## Install @@ -56,6 +64,38 @@ Restart the app automatically every time code changes. Useful during development $ npm run watch ``` +### Run linters + +``` +$ npm test +``` + +### Run integration tests + +``` +$ npm run test-integration +``` + +The integration tests use Spectron and Tape. They click through the app, taking screenshots and +comparing each one to a reference. Why screenshots? + +* Ad-hoc checking makes the tests a lot more work to write +* Even diffing the whole HTML is not as thorough as screenshot diffing. For example, it wouldn't + catch an bug where hitting ESC from a video doesn't correctly restore window size. +* Chrome's own integration tests use screenshot diffing iirc +* Small UI changes will break a few tests, but the fix is as easy as deleting the offending + screenshots and running the tests, which will recreate them with the new look. +* The resulting Github PR will then show, pixel by pixel, the exact UI changes that were made! See + https://github.com/blog/817-behold-image-view-modes + +For MacOS, you'll need a Retina screen for the integration tests to pass. Your screen should have +the same resolution as a 2016 12" Macbook. + +For Windows, you'll need Windows 10 with a 1366x768 screen. + +When running integration tests, keep the mouse on the edge of the screen and don't touch the mouse +or keyboard while the tests are running. + ### Package the app Builds app binaries for Mac, Linux, and Windows. @@ -84,7 +124,7 @@ The following optional arguments are available: - `all` - All platforms (default) Note: Even with the `--package` option, the auto-update files (.nupkg for Windows, -*-darwin.zip for Mac) will always be produced. +-darwin.zip for Mac) will always be produced. #### Windows build notes diff --git a/bin/check-deps.js b/bin/check-deps.js deleted file mode 100755 index 1423f73c..00000000 --- a/bin/check-deps.js +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env node - -var fs = require('fs') -var cp = require('child_process') - -// We can't use `builtin-modules` here since our TravisCI -// setup expects this file to run with no dependencies -var BUILT_IN_NODE_MODULES = [ - 'assert', - 'buffer', - 'child_process', - 'cluster', - 'console', - 'constants', - 'crypto', - 'dgram', - 'dns', - 'domain', - 'events', - 'fs', - 'http', - 'https', - 'module', - 'net', - 'os', - 'path', - 'process', - 'punycode', - 'querystring', - 'readline', - 'repl', - 'stream', - 'string_decoder', - 'timers', - 'tls', - 'tty', - 'url', - 'util', - 'v8', - 'vm', - 'zlib' -] - -var BUILT_IN_ELECTRON_MODULES = [ 'electron' ] - -var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES) - -var EXECUTABLE_DEPS = [ - 'babel-cli', - 'babel-plugin-syntax-jsx', - 'babel-plugin-transform-es2015-destructuring', - 'babel-plugin-transform-object-rest-spread', - 'babel-plugin-transform-react-jsx', - 'gh-release', - 'nodemon', - 'standard' -] - -main() - -// Scans codebase for missing or unused dependencies. Exits with code 0 on success. -function main () { - if (process.platform === 'win32') { - console.error('Sorry, check-deps only works on Mac and Linux') - return - } - - var usedDeps = findUsedDeps() - var packageDeps = findPackageDeps() - - var missingDeps = usedDeps.filter( - (dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep) - ) - var unusedDeps = packageDeps.filter( - (dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep) - ) - - if (missingDeps.length > 0) { - console.error('Missing package dependencies: ' + missingDeps) - } - if (unusedDeps.length > 0) { - console.error('Unused package dependencies: ' + unusedDeps) - } - if (missingDeps.length + unusedDeps.length > 0) { - process.exitCode = 1 - } -} - -// Finds all dependencies specified in `package.json` -function findPackageDeps () { - var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')) - - var deps = Object.keys(pkg.dependencies) - var devDeps = Object.keys(pkg.devDependencies) - var optionalDeps = Object.keys(pkg.optionalDependencies) - - return [].concat(deps, devDeps, optionalDeps) -} - -// Finds all dependencies that used with `require()` -function findUsedDeps () { - var stdout = cp.execSync('./bin/list-deps.sh') - return stdout.toString().trim().split('\n') -} - -function includes (arr, elem) { - return arr.indexOf(elem) >= 0 -} diff --git a/bin/clean.js b/bin/clean.js index 2bf78c24..d094dc16 100755 --- a/bin/clean.js +++ b/bin/clean.js @@ -5,13 +5,13 @@ * Useful for developers. */ -var fs = require('fs') -var os = require('os') -var path = require('path') -var rimraf = require('rimraf') +const fs = require('fs') +const os = require('os') +const path = require('path') +const rimraf = require('rimraf') -var config = require('../src/config') -var handlers = require('../src/main/handlers') +const config = require('../src/config') +const handlers = require('../src/main/handlers') // First, remove generated files rimraf.sync('build/') @@ -21,11 +21,11 @@ rimraf.sync('dist/') rimraf.sync(config.CONFIG_PATH) // Remove any temporary files -var tmpPath +let tmpPath try { tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent') } catch (err) { - tmpPath = path.join(os.tmpDir(), 'webtorrent') + tmpPath = path.join(os.tmpdir(), 'webtorrent') } rimraf.sync(tmpPath) diff --git a/bin/cmd.js b/bin/cmd.js deleted file mode 100755 index 34b6a91f..00000000 --- a/bin/cmd.js +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env node - -var electron = require('electron') -var cp = require('child_process') -var path = require('path') - -var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'}) -child.on('close', function (code) { - process.exitCode = code -}) diff --git a/bin/extra-lint.js b/bin/extra-lint.js new file mode 100755 index 00000000..5f39cd91 --- /dev/null +++ b/bin/extra-lint.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +const walkSync = require('walk-sync') +const fs = require('fs') +const path = require('path') + +let hasErrors = false + +// Find all Javascript source files +const files = walkSync('src', {globs: ['**/*.js']}) +console.log('Running extra-lint on ' + files.length + ' files...') + +// Read each file, line by line +files.forEach(function (file) { + const filepath = path.join('src', file) + const lines = fs.readFileSync(filepath, 'utf8').split('\n') + + lines.forEach(function (line, i) { + let error + + // Consistent JSX tag closing + if (line.match(/' {2}\/> *$/) || + line.match('[^ ]/> *$') || + line.match(' > *$')) { + error = 'JSX tag spacing' + } + + // No lines over 100 characters + if (line.length > 100) { + error = 'Line >100 chars' + } + + if (line.match(/^var /) || line.match(/ var /)) { + error = 'Use const or let' + } + + if (error) { + let name = path.basename(file) + console.log('%s:%d - %s:\n%s', name, i + 1, error, line) + hasErrors = true + } + }) +}) + +if (hasErrors) process.exit(1) +else console.log('Looks good!') diff --git a/bin/list-deps.sh b/bin/list-deps.sh deleted file mode 100755 index b1ff6c32..00000000 --- a/bin/list-deps.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -# This is a truly heinous hack, but it works pretty nicely. -# Find all modules we're requiring---even conditional requires. - -grep "require('" src/ bin/ -R | - grep '.js:' | - sed "s/.*require('\([^'\/]*\).*/\1/" | - grep -v '^\.' | - sort | - uniq diff --git a/bin/open-config.js b/bin/open-config.js index e2756e15..336ad4b2 100755 --- a/bin/open-config.js +++ b/bin/open-config.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -var config = require('../src/config') -var open = require('open') +const config = require('../src/config') +const open = require('open') open(config.CONFIG_PATH) diff --git a/bin/package.js b/bin/package.js index c5f73b6b..9f59a193 100755 --- a/bin/package.js +++ b/bin/package.js @@ -1,28 +1,29 @@ #!/usr/bin/env node /** - * Builds app binaries for Mac, Linux, and Windows. + * Builds app binaries for Mac, Windows, and Linux. */ -var cp = require('child_process') -var electronPackager = require('electron-packager') -var fs = require('fs') -var minimist = require('minimist') -var mkdirp = require('mkdirp') -var os = require('os') -var path = require('path') -var rimraf = require('rimraf') -var series = require('run-series') -var zip = require('cross-zip') +const cp = require('child_process') +const electronPackager = require('electron-packager') +const fs = require('fs') +const minimist = require('minimist') +const mkdirp = require('mkdirp') +const os = require('os') +const path = require('path') +const rimraf = require('rimraf') +const series = require('run-series') +const zip = require('cross-zip') -var config = require('../src/config') -var pkg = require('../package.json') +const config = require('../src/config') +const pkg = require('../package.json') -var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION -var BUILD_PATH = path.join(config.ROOT_PATH, 'build') -var DIST_PATH = path.join(config.ROOT_PATH, 'dist') +const BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION +const BUILD_PATH = path.join(config.ROOT_PATH, 'build') +const DIST_PATH = path.join(config.ROOT_PATH, 'dist') +const NODE_MODULES_PATH = path.join(config.ROOT_PATH, 'node_modules') -var argv = minimist(process.argv.slice(2), { +const argv = minimist(process.argv.slice(2), { boolean: [ 'sign' ], @@ -36,14 +37,20 @@ var argv = minimist(process.argv.slice(2), { }) function build () { + console.log('Reinstalling node_modules...') + rimraf.sync(NODE_MODULES_PATH) + cp.execSync('npm install', { stdio: 'inherit' }) + cp.execSync('npm dedupe', { stdio: 'inherit' }) + + console.log('Nuking dist/ and build/...') rimraf.sync(DIST_PATH) rimraf.sync(BUILD_PATH) - console.log('Babel: Building JSX...') + console.log('Build: Transpiling to ES5...') cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' }) - console.log('Babel: Built JSX.') + console.log('Build: Transpiled to ES5.') - var platform = argv._[0] + const platform = argv._[0] if (platform === 'darwin') { buildDarwin(printDone) } else if (platform === 'win32') { @@ -61,7 +68,7 @@ function build () { } } -var all = { +const all = { // The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata // property on Windows, and `NSHumanReadableCopyright` on Mac. 'app-copyright': config.APP_COPYRIGHT, @@ -73,11 +80,11 @@ var all = { // Package the application's source code into an archive, using Electron's archive // format. Mitigates issues around long path names on Windows and slightly speeds up // require(). - asar: true, - - // A glob expression, that unpacks the files with matching names to the - // "app.asar.unpacked" directory. - 'asar-unpack': 'WebTorrent*', + asar: { + // A glob expression, that unpacks the files with matching names to the + // "app.asar.unpacked" directory. + unpack: 'WebTorrent*' + }, // The build version of the application. Maps to the FileVersion metadata property on // Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to @@ -89,7 +96,7 @@ var all = { // Pattern which specifies which files to ignore when copying files to create the // package(s). - ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/, + ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^/]*|.*\.md|.*\.markdown)$/, // The application name. name: config.APP_NAME, @@ -104,15 +111,15 @@ var all = { // "devDependencies" before starting to package the app. prune: true, - // The Electron version with which the app is built (without the leading 'v') - version: require('electron/package.json').version + // The Electron version that the app is built with (without the leading 'v') + electronVersion: require('electron/package.json').version } -var darwin = { +const darwin = { // Build for Mac platform: 'darwin', - // Build 64 bit binaries only. + // Build x64 binaries only. arch: 'x64', // The bundle identifier to use in the application's plist (Mac only). @@ -129,15 +136,15 @@ var darwin = { icon: config.APP_ICON + '.icns' } -var win32 = { +const win32 = { // Build for Windows. platform: 'win32', - // Build 32 bit binaries only. - arch: 'ia32', + // Build ia32 and x64 binaries. + arch: ['ia32', 'x64'], // Object hash of application metadata to embed into the executable (Windows only) - 'version-string': { + win32metadata: { // Company that produced the file. CompanyName: config.APP_NAME, @@ -163,12 +170,12 @@ var win32 = { icon: config.APP_ICON + '.ico' } -var linux = { +const linux = { // Build for Linux. platform: 'linux', - // Build 32 and 64 bit binaries. - arch: 'all' + // Build ia32 and x64 binaries. + arch: ['ia32', 'x64'] // Note: Application icon for Linux is specified via the BrowserWindow `icon` option. } @@ -176,18 +183,18 @@ var linux = { build() function buildDarwin (cb) { - var plist = require('plist') + const plist = require('plist') console.log('Mac: Packaging electron...') electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { if (err) return cb(err) console.log('Mac: Packaged electron. ' + buildPath) - var appPath = path.join(buildPath[0], config.APP_NAME + '.app') - var contentsPath = path.join(appPath, 'Contents') - var resourcesPath = path.join(contentsPath, 'Resources') - var infoPlistPath = path.join(contentsPath, 'Info.plist') - var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) + const appPath = path.join(buildPath[0], config.APP_NAME + '.app') + const contentsPath = path.join(appPath, 'Contents') + const resourcesPath = path.join(contentsPath, 'Resources') + const infoPlistPath = path.join(contentsPath, 'Info.plist') + const infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) infoPlist.CFBundleDocumentTypes = [ { @@ -261,7 +268,7 @@ function buildDarwin (cb) { } function signApp (cb) { - var sign = require('electron-osx-sign') + const sign = require('electron-osx-sign') /* * Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons: @@ -276,7 +283,7 @@ function buildDarwin (cb) { * - Xcode Command Line Tools (xcode-select --install) * - Membership in the Apple Developer Program */ - var signOpts = { + const signOpts = { app: appPath, platform: 'darwin', verbose: true @@ -302,8 +309,8 @@ function buildDarwin (cb) { // Create .zip file (used by the auto-updater) console.log('Mac: Creating zip...') - var inPath = path.join(buildPath[0], config.APP_NAME + '.app') - var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip') + const inPath = path.join(buildPath[0], config.APP_NAME + '.app') + const outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip') zip.zipSync(inPath, outPath) console.log('Mac: Created zip.') @@ -312,13 +319,13 @@ function buildDarwin (cb) { function packageDmg (cb) { console.log('Mac: Creating dmg...') - var appDmg = require('appdmg') + const appDmg = require('appdmg') - var targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg') + const targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg') rimraf.sync(targetPath) // Create a .dmg (Mac disk image) file, for easy user installation. - var dmgOpts = { + const dmgOpts = { basepath: config.ROOT_PATH, target: targetPath, specification: { @@ -339,7 +346,7 @@ function buildDarwin (cb) { } } - var dmg = appDmg(dmgOpts) + const dmg = appDmg(dmgOpts) dmg.once('error', cb) dmg.on('progress', function (info) { if (info.type === 'step-begin') console.log(info.title + '...') @@ -353,7 +360,7 @@ function buildDarwin (cb) { } function buildWin32 (cb) { - var installer = require('electron-winstaller') + const installer = require('electron-winstaller') console.log('Windows: Packaging electron...') /* @@ -361,7 +368,7 @@ function buildWin32 (cb) { * - Windows Authenticode private key and cert (authenticode.p12) * - Windows Authenticode password file (authenticode.txt) */ - var CERT_PATH + let CERT_PATH try { fs.accessSync('D:') CERT_PATH = 'D:' @@ -373,12 +380,12 @@ function buildWin32 (cb) { if (err) return cb(err) console.log('Windows: Packaged electron. ' + buildPath) - var signWithParams + let signWithParams if (process.platform === 'win32') { if (argv.sign) { - var certificateFile = path.join(CERT_PATH, 'authenticode.p12') - var certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8') - var timestampServer = 'http://timestamp.comodoca.com' + const certificateFile = path.join(CERT_PATH, 'authenticode.p12') + const certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8') + const timestampServer = 'http://timestamp.comodoca.com' signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256` } else { printWarning() @@ -387,20 +394,26 @@ function buildWin32 (cb) { printWarning() } - var tasks = [] - if (argv.package === 'exe' || argv.package === 'all') { - tasks.push((cb) => packageInstaller(cb)) - } - if (argv.package === 'portable' || argv.package === 'all') { - tasks.push((cb) => packagePortable(cb)) - } + const tasks = [] + buildPath.forEach(function (filesPath) { + const destArch = filesPath.split('-').pop() + + if (argv.package === 'exe' || argv.package === 'all') { + tasks.push((cb) => packageInstaller(filesPath, destArch, cb)) + } + if (argv.package === 'portable' || argv.package === 'all') { + tasks.push((cb) => packagePortable(filesPath, destArch, cb)) + } + }) series(tasks, cb) - function packageInstaller (cb) { - console.log('Windows: Creating installer...') + function packageInstaller (filesPath, destArch, cb) { + console.log(`Windows: Creating ${destArch} installer...`) + + const archStr = destArch === 'ia32' ? '-ia32' : '' installer.createWindowsInstaller({ - appDirectory: buildPath[0], + appDirectory: filesPath, authors: config.APP_TEAM, description: config.APP_NAME, exe: config.APP_NAME + '.exe', @@ -410,8 +423,26 @@ function buildWin32 (cb) { noMsi: true, outputDirectory: DIST_PATH, productName: config.APP_NAME, - remoteReleases: config.GITHUB_URL, - setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe', + /** + * Only create delta updates for the Windows x64 build because 90% of our + * users have Windows x64 and the delta files take a *very* long time to + * generate. Also, the ia32 files on GitHub have non-standard Squirrel + * names (i.e. RELEASES-ia32 instead of RELEASES) and so Squirrel won't + * find them unless we proxy the requests. + */ + // TODO: Re-enable Windows 64-bit delta updates when we confirm that they + // work correctly in the presence of the "ia32" .nupkg files. I + // (feross) noticed them listed in the 64-bit RELEASES file and + // manually edited them out for the v0.17 release. Shipping only + // full updates for now will work fine, with no ill-effects. + // remoteReleases: destArch === 'x64' + // ? config.GITHUB_URL + // : undefined, + /** + * If you hit a "GitHub API rate limit exceeded" error, set this token! + */ + // remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN, + setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe', setupIcon: config.APP_ICON + '.ico', signWithParams: signWithParams, title: config.APP_NAME, @@ -419,23 +450,71 @@ function buildWin32 (cb) { version: pkg.version }) .then(function () { - console.log('Windows: Created installer.') + console.log(`Windows: Created ${destArch} installer.`) + + /** + * Delete extraneous Squirrel files (i.e. *.nupkg delta files for older + * versions of the app) + */ + fs.readdirSync(DIST_PATH) + .filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version)) + .forEach((filename) => { + fs.unlinkSync(path.join(DIST_PATH, filename)) + }) + + if (destArch === 'ia32') { + console.log('Windows: Renaming ia32 installer files...') + + // RELEASES -> RELEASES-ia32 + const relPath = path.join(DIST_PATH, 'RELEASES-ia32') + fs.renameSync( + path.join(DIST_PATH, 'RELEASES'), + relPath + ) + + // WebTorrent-vX.X.X-full.nupkg -> WebTorrent-vX.X.X-ia32-full.nupkg + fs.renameSync( + path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-full.nupkg`), + path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-ia32-full.nupkg`) + ) + + // Change file name inside RELEASES-ia32 to match renamed file + const relContent = fs.readFileSync(relPath, 'utf8') + const relContent32 = relContent.replace('full.nupkg', 'ia32-full.nupkg') + fs.writeFileSync(relPath, relContent32) + + if (relContent === relContent32) { + // Sanity check + throw new Error('Fixing RELEASES-ia32 failed. Replacement did not modify the file.') + } + + console.log('Windows: Renamed ia32 installer files.') + } + cb(null) }) .catch(cb) } - function packagePortable (cb) { - console.log('Windows: Creating portable app...') + function packagePortable (filesPath, destArch, cb) { + console.log(`Windows: Creating ${destArch} portable app...`) - var portablePath = path.join(buildPath[0], 'Portable Settings') + const portablePath = path.join(filesPath, 'Portable Settings') mkdirp.sync(portablePath) - var inPath = path.join(DIST_PATH, path.basename(buildPath[0])) - var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip') + const downloadsPath = path.join(portablePath, 'Downloads') + mkdirp.sync(downloadsPath) + + const tempPath = path.join(portablePath, 'Temp') + mkdirp.sync(tempPath) + + const archStr = destArch === 'ia32' ? '-ia32' : '' + + const inPath = path.join(DIST_PATH, path.basename(filesPath)) + const outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip') zip.zipSync(inPath, outPath) - console.log('Windows: Created portable app.') + console.log(`Windows: Created ${destArch} portable app.`) cb(null) } }) @@ -447,9 +526,9 @@ function buildLinux (cb) { if (err) return cb(err) console.log('Linux: Packaged electron. ' + buildPath) - var tasks = [] + const tasks = [] buildPath.forEach(function (filesPath) { - var destArch = filesPath.split('-').pop() + const destArch = filesPath.split('-').pop() if (argv.package === 'deb' || argv.package === 'all') { tasks.push((cb) => packageDeb(filesPath, destArch, cb)) @@ -465,8 +544,8 @@ function buildLinux (cb) { // Create .deb file for Debian-based platforms console.log(`Linux: Creating ${destArch} deb...`) - var deb = require('nobin-debian-installer')() - var destPath = path.join('/opt', pkg.name) + const deb = require('nobin-debian-installer')() + const destPath = path.join('/opt', pkg.name) deb.pack({ package: pkg, @@ -500,8 +579,10 @@ function buildLinux (cb) { // Create .zip file for Linux console.log(`Linux: Creating ${destArch} zip...`) - var inPath = path.join(DIST_PATH, path.basename(filesPath)) - var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip') + const archStr = destArch === 'ia32' ? '-ia32' : '' + + const inPath = path.join(DIST_PATH, path.basename(filesPath)) + const outPath = path.join(DIST_PATH, BUILD_NAME + '-linux' + archStr + '.zip') zip.zipSync(inPath, outPath) console.log(`Linux: Created ${destArch} zip.`) diff --git a/bin/release-_post.sh b/bin/release-_post.sh deleted file mode 100755 index c1ca2f1d..00000000 --- a/bin/release-_post.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -set -e - -npm run update-authors -git diff --exit-code -npm run package -- --sign -git push -git push --tags -npm publish -./node_modules/.bin/gh-release diff --git a/bin/release-_pre.sh b/bin/release-_pre.sh deleted file mode 100755 index 230d66a6..00000000 --- a/bin/release-_pre.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -e - -git pull -rm -rf node_modules/ -npm install -npm dedupe -npm test diff --git a/bin/release-major.sh b/bin/release-major.sh deleted file mode 100755 index 85efc1d2..00000000 --- a/bin/release-major.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -set -e -BIN=`dirname $0` - -$BIN/release-_pre.sh -npm version major -$BIN/release-_post.sh diff --git a/bin/release-minor.sh b/bin/release-minor.sh deleted file mode 100755 index 8c1cbd36..00000000 --- a/bin/release-minor.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -set -e -BIN=`dirname $0` - -$BIN/release-_pre.sh -npm version minor -$BIN/release-_post.sh diff --git a/bin/release-patch.sh b/bin/release-patch.sh deleted file mode 100755 index 62fd7fcc..00000000 --- a/bin/release-patch.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -set -e -BIN=`dirname $0` - -$BIN/release-_pre.sh -npm version patch -$BIN/release-_post.sh diff --git a/package.json b/package.json index aa6fa447..90be9826 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,44 @@ { "name": "webtorrent-desktop", "description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.", - "version": "0.12.0", + "version": "0.18.0", "author": { "name": "WebTorrent, LLC", "email": "feross@webtorrent.io", "url": "https://webtorrent.io" }, - "bin": { - "webtorrent-desktop": "./bin/cmd.js" - }, "bugs": { "url": "https://github.com/feross/webtorrent-desktop/issues" }, "dependencies": { "airplayer": "^2.0.0", "application-config": "^1.0.0", + "arch": "^2.0.0", + "auto-launch": "^4.0.1", "bitfield": "^1.0.2", + "capture-frame": "^1.0.0", "chromecasts": "^1.8.0", + "cp-file": "^4.0.1", "create-torrent": "^3.24.5", + "debounce": "^1.0.0", "deep-equal": "^1.0.1", "dlnacasts": "^0.1.0", "drag-drop": "^2.12.1", - "electron": "1.3.3", - "fs-extra": "^0.30.0", + "es6-error": "^4.0.0", + "fn-getter": "^1.0.0", "iso-639-1": "^1.2.1", "languagedetect": "^1.1.1", "location-history": "^1.0.0", - "material-ui": "^0.15.4", + "material-ui": "^0.17.0", + "mkdirp": "^0.5.1", "musicmetadata": "^2.0.2", "network-address": "^1.1.0", "parse-torrent": "^5.7.3", "prettier-bytes": "^1.0.1", - "react": "^15.2.1", - "react-dom": "^15.2.1", - "react-tap-event-plugin": "^1.0.0", + "react": "^15.4.2", + "react-dom": "^15.4.2", + "react-tap-event-plugin": "^2.0.1", + "rimraf": "^2.5.2", "run-parallel": "^1.1.6", "semver": "^5.1.0", "simple-concat": "^1.0.0", @@ -46,25 +50,25 @@ "zero-fill": "^2.2.3" }, "devDependencies": { - "babel-cli": "^6.11.4", - "babel-plugin-syntax-jsx": "^6.13.0", - "babel-plugin-transform-es2015-destructuring": "^6.9.0", - "babel-plugin-transform-object-rest-spread": "^6.8.0", - "babel-plugin-transform-react-jsx": "^6.8.0", + "buble": "^0.15.2", "cross-zip": "^2.0.1", - "electron-osx-sign": "^0.3.0", - "electron-packager": "^7.0.0", - "electron-winstaller": "^2.3.0", + "depcheck": "^0.6.4", + "electron": "1.6.0", + "electron-osx-sign": "0.4.3", + "electron-packager": "~8.5.1", + "electron-winstaller": "~2.5.2", "gh-release": "^2.0.3", "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "nobin-debian-installer": "^0.0.10", + "nobin-debian-installer": "0.0.10", "nodemon": "^1.10.2", "open": "0.0.5", "plist": "^2.0.1", - "rimraf": "^2.5.2", + "pngjs": "^3.0.0", "run-series": "^1.1.4", - "standard": "*" + "spectron": "^3.3.0", + "standard": "*", + "tape": "^4.6.0", + "walk-sync": "^0.3.1" }, "engines": { "node": ">=4.0.0" @@ -85,20 +89,23 @@ "optionalDependencies": { "appdmg": "^0.4.3" }, + "private": true, "productName": "WebTorrent", "repository": { "type": "git", "url": "git://github.com/feross/webtorrent-desktop.git" }, "scripts": { - "build": "babel --quiet src --out-dir build", + "build": "buble src --output build", "clean": "node ./bin/clean.js", + "gh-release": "gh-release", "open-config": "node ./bin/open-config.js", "package": "node ./bin/package.js", "prepublish": "npm run build", "start": "npm run build && electron .", - "test": "standard && node ./bin/check-deps.js", + "test": "standard && depcheck --ignores=buble,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js", + "test-integration": "npm run build && node ./test", "update-authors": "./bin/update-authors.sh", - "watch": "nodemon --exec 'npm run start' --ext js,pug,css" + "watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/" } } diff --git a/src/.babelrc b/src/.babelrc deleted file mode 100644 index db07c35e..00000000 --- a/src/.babelrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "plugins": [ - "syntax-jsx", - "transform-es2015-destructuring", - "transform-object-rest-spread", - "transform-react-jsx" - ] -} diff --git a/src/config.js b/src/config.js index 2848ea68..a7d4d9c1 100644 --- a/src/config.js +++ b/src/config.js @@ -1,12 +1,21 @@ -var appConfig = require('application-config')('WebTorrent') -var fs = require('fs') -var path = require('path') +const appConfig = require('application-config')('WebTorrent') +const path = require('path') +const electron = require('electron') +const arch = require('arch') -var APP_NAME = 'WebTorrent' -var APP_TEAM = 'WebTorrent, LLC' -var APP_VERSION = require('../package.json').version +const APP_NAME = 'WebTorrent' +const APP_TEAM = 'WebTorrent, LLC' +const APP_VERSION = require('../package.json').version -var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings') +const IS_TEST = isTest() +const PORTABLE_PATH = IS_TEST + ? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest') + : path.join(path.dirname(process.execPath), 'Portable Settings') +const IS_PRODUCTION = isProduction() +const IS_PORTABLE = isPortable() + +const UI_HEADER_HEIGHT = 38 +const UI_TORRENT_HEIGHT = 100 module.exports = { ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement', @@ -14,7 +23,7 @@ module.exports = { CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report', TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry', - APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, + APP_COPYRIGHT: 'Copyright © 2014-2017 ' + APP_TEAM, APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'), APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'), APP_NAME: APP_NAME, @@ -26,27 +35,32 @@ module.exports = { DEFAULT_TORRENTS: [ { + testID: 'bbb', name: 'Big Buck Bunny', posterFileName: 'bigBuckBunny.jpg', torrentFileName: 'bigBuckBunny.torrent' }, { + testID: 'cosmos', name: 'Cosmos Laundromat (Preview)', posterFileName: 'cosmosLaundromat.jpg', torrentFileName: 'cosmosLaundromat.torrent' }, { + testID: 'sintel', name: 'Sintel', posterFileName: 'sintel.jpg', torrentFileName: 'sintel.torrent' }, { + testID: 'tears', name: 'Tears of Steel', posterFileName: 'tearsOfSteel.jpg', torrentFileName: 'tearsOfSteel.torrent' }, { - name: 'The WIRED CD - Rip. Sample. Mash. Share.', + testID: 'wired', + name: 'The WIRED CD - Rip. Sample. Mash. Share', posterFileName: 'wiredCd.jpg', torrentFileName: 'wiredCd.torrent' } @@ -62,8 +76,11 @@ module.exports = { HOME_PAGE_URL: 'https://webtorrent.io', - IS_PORTABLE: isPortable(), - IS_PRODUCTION: isProduction(), + IS_PORTABLE: IS_PORTABLE, + IS_PRODUCTION: IS_PRODUCTION, + IS_TEST: IS_TEST, + + OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32', POSTER_PATH: path.join(getConfigPath(), 'Posters'), ROOT_PATH: path.join(__dirname, '..'), @@ -74,12 +91,19 @@ module.exports = { WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'), WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'), - WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents - WINDOW_MIN_WIDTH: 425 + WINDOW_INITIAL_BOUNDS: { + width: 500, + height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents + }, + WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents + WINDOW_MIN_WIDTH: 425, + + UI_HEADER_HEIGHT: UI_HEADER_HEIGHT, + UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT } function getConfigPath () { - if (isPortable()) { + if (IS_PORTABLE) { return PORTABLE_PATH } else { return path.dirname(appConfig.filePath) @@ -87,24 +111,47 @@ function getConfigPath () { } function getDefaultDownloadPath () { - if (!process || !process.type) { - return '' - } - - if (isPortable()) { + if (IS_PORTABLE) { return path.join(getConfigPath(), 'Downloads') + } else { + return getPath('downloads') } +} - var electron = require('electron') +function getPath (key) { + if (!process.versions.electron) { + // Node.js process + return '' + } else if (process.type === 'renderer') { + // Electron renderer process + return electron.remote.app.getPath(key) + } else { + // Electron main process + return electron.app.getPath(key) + } +} - return process.type === 'renderer' - ? electron.remote.app.getPath('downloads') - : electron.app.getPath('downloads') +function isTest () { + return process.env.NODE_ENV === 'test' } function isPortable () { + if (IS_TEST) { + return true + } + + if (process.platform !== 'win32' || !IS_PRODUCTION) { + // Fast path: Non-Windows platforms should not check for path on disk + return false + } + + const fs = require('fs') + try { - return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH) + // This line throws if the "Portable Settings" folder does not exist, and does + // nothing otherwise. + fs.accessSync(PORTABLE_PATH, fs.constants.R_OK | fs.constants.W_OK) + return true } catch (err) { return false } @@ -112,6 +159,7 @@ function isPortable () { function isProduction () { if (!process.versions.electron) { + // Node.js process return false } if (process.platform === 'darwin') { diff --git a/src/crash-reporter.js b/src/crash-reporter.js index 359f5645..8fd2ac15 100644 --- a/src/crash-reporter.js +++ b/src/crash-reporter.js @@ -2,10 +2,10 @@ module.exports = { init } -var config = require('./config') -var electron = require('electron') - function init () { + const config = require('./config') + const electron = require('electron') + electron.crashReporter.start({ companyName: config.APP_NAME, productName: config.APP_NAME, diff --git a/src/main/announcement.js b/src/main/announcement.js index 9f7155d0..662ce51c 100644 --- a/src/main/announcement.js +++ b/src/main/announcement.js @@ -2,12 +2,12 @@ module.exports = { init } -var electron = require('electron') +const electron = require('electron') -var config = require('../config') -var log = require('./log') +const config = require('../config') +const log = require('./log') -var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL + +const ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL + '?version=' + config.APP_VERSION + '&platform=' + process.platform @@ -26,7 +26,7 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL + * } */ function init () { - var get = require('simple-get') + const get = require('simple-get') get.concat(ANNOUNCEMENT_URL, onResponse) } diff --git a/src/main/dialog.js b/src/main/dialog.js index b1ef94a8..38b9c96b 100644 --- a/src/main/dialog.js +++ b/src/main/dialog.js @@ -6,10 +6,10 @@ module.exports = { openFiles } -var electron = require('electron') +const electron = require('electron') -var log = require('./log') -var windows = require('./windows') +const log = require('./log') +const windows = require('./windows') /** * Show open dialog to create a single-file torrent. @@ -17,16 +17,11 @@ var windows = require('./windows') function openSeedFile () { if (!windows.main.win) return log('openSeedFile') - var opts = { + const opts = { title: 'Select a file for the torrent.', properties: [ 'openFile' ] } - setTitle(opts.title) - electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { - resetTitle() - if (!Array.isArray(selectedPaths)) return - windows.main.dispatch('showCreateTorrent', selectedPaths) - }) + showOpenSeed(opts) } /* @@ -37,7 +32,7 @@ function openSeedFile () { function openSeedDirectory () { if (!windows.main.win) return log('openSeedDirectory') - var opts = process.platform === 'darwin' + const opts = process.platform === 'darwin' ? { title: 'Select a file or folder for the torrent.', properties: [ 'openFile', 'openDirectory' ] @@ -46,12 +41,7 @@ function openSeedDirectory () { title: 'Select a folder for the torrent.', properties: [ 'openDirectory' ] } - setTitle(opts.title) - electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { - resetTitle() - if (!Array.isArray(selectedPaths)) return - windows.main.dispatch('showCreateTorrent', selectedPaths) - }) + showOpenSeed(opts) } /* @@ -61,7 +51,7 @@ function openSeedDirectory () { function openFiles () { if (!windows.main.win) return log('openFiles') - var opts = process.platform === 'darwin' + const opts = process.platform === 'darwin' ? { title: 'Select a file or folder to add.', properties: [ 'openFile', 'openDirectory' ] @@ -84,7 +74,7 @@ function openFiles () { function openTorrentFile () { if (!windows.main.win) return log('openTorrentFile') - var opts = { + const opts = { title: 'Select a .torrent file.', filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], properties: [ 'openFile', 'multiSelections' ] @@ -119,3 +109,16 @@ function setTitle (title) { function resetTitle () { windows.main.dispatch('resetTitle') } + +/** + * Pops up an Open File dialog with the given options. + * After the user selects files / folders, shows the Create Torrent page. + */ +function showOpenSeed (opts) { + setTitle(opts.title) + electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { + resetTitle() + if (!Array.isArray(selectedPaths)) return + windows.main.dispatch('showCreateTorrent', selectedPaths) + }) +} diff --git a/src/main/dock.js b/src/main/dock.js index be05ac34..1eaabd7d 100644 --- a/src/main/dock.js +++ b/src/main/dock.js @@ -4,19 +4,17 @@ module.exports = { setBadge } -var electron = require('electron') +const {app, Menu} = require('electron') -var app = electron.app - -var dialog = require('./dialog') -var log = require('./log') +const dialog = require('./dialog') +const log = require('./log') /** * Add a right-click menu to the dock icon. (Mac) */ function init () { if (!app.dock) return - var menu = electron.Menu.buildFromTemplate(getMenuTemplate()) + const menu = Menu.buildFromTemplate(getMenuTemplate()) app.dock.setMenu(menu) } @@ -33,8 +31,11 @@ function downloadFinished (path) { * Display a counter badge for the app. (Mac, Linux) */ function setBadge (count) { - log(`setBadge: ${count}`) - app.setBadgeCount(Number(count)) + if (process.platform === 'darwin' || + (process.platform === 'linux' && app.isUnityRunning())) { + log(`setBadge: ${count}`) + app.setBadgeCount(Number(count)) + } } function getMenuTemplate () { diff --git a/src/main/external-player.js b/src/main/external-player.js index 57f4487f..39b80061 100644 --- a/src/main/external-player.js +++ b/src/main/external-player.js @@ -4,29 +4,36 @@ module.exports = { checkInstall } -var cp = require('child_process') -var vlcCommand = require('vlc-command') +const cp = require('child_process') +const path = require('path') +const vlcCommand = require('vlc-command') -var log = require('./log') -var windows = require('./windows') +const log = require('./log') +const windows = require('./windows') // holds a ChildProcess while we're playing a video in an external player, null otherwise -var proc +let proc = null -function checkInstall (path, cb) { +function checkInstall (playerPath, cb) { // check for VLC if external player has not been specified by the user // otherwise assume the player is installed - if (path == null) return vlcCommand((err) => cb(!err)) - process.nextTick(() => cb(true)) + if (playerPath == null) return vlcCommand(cb) + process.nextTick(() => cb(null)) } -function spawn (path, url, title) { - if (path != null) return spawnExternal(path, [url]) +function spawn (playerPath, url, title) { + if (playerPath != null) return spawnExternal(playerPath, [url]) // Try to find and use VLC if external player is not specified vlcCommand(function (err, vlcPath) { if (err) return windows.main.dispatch('externalPlayerNotFound') - var args = ['--play-and-exit', '--video-on-top', '--quiet', `--meta-title=${JSON.stringify(title)}`, url] + const args = [ + '--play-and-exit', + '--video-on-top', + '--quiet', + `--meta-title=${JSON.stringify(title)}`, + url + ] spawnExternal(vlcPath, args) }) } @@ -38,13 +45,18 @@ function kill () { proc = null } -function spawnExternal (path, args) { - log('Running external media player:', path + ' ' + args.join(' ')) +function spawnExternal (playerPath, args) { + log('Running external media player:', playerPath + ' ' + args.join(' ')) - proc = cp.spawn(path, args, {stdio: 'ignore'}) + if (process.platform === 'darwin' && path.extname(playerPath) === '.app') { + // Mac: Use executable in packaged .app bundle + playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app') + } + + proc = cp.spawn(playerPath, args, {stdio: 'ignore'}) // If it works, close the modal after a second - var closeModalTimeout = setTimeout(() => + const closeModalTimeout = setTimeout(() => windows.main.dispatch('exitModal'), 1000) proc.on('close', function (code) { diff --git a/src/main/handlers.js b/src/main/handlers.js index eb2f18a6..e3dff9a9 100644 --- a/src/main/handlers.js +++ b/src/main/handlers.js @@ -3,8 +3,8 @@ module.exports = { uninstall } -var config = require('../config') -var path = require('path') +const config = require('../config') +const path = require('path') function install () { if (process.platform === 'darwin') { @@ -31,8 +31,8 @@ function uninstall () { } function installDarwin () { - var electron = require('electron') - var app = electron.app + const electron = require('electron') + const app = electron.app // On Mac, only protocols that are listed in `Info.plist` can be set as the // default handler at runtime. @@ -44,18 +44,18 @@ function installDarwin () { function uninstallDarwin () {} -var EXEC_COMMAND = [ process.execPath ] +const EXEC_COMMAND = [ process.execPath ] if (!config.IS_PRODUCTION) { EXEC_COMMAND.push(config.ROOT_PATH) } function installWin32 () { - var Registry = require('winreg') + const Registry = require('winreg') - var log = require('./log') + const log = require('./log') - var iconPath = path.join( + const iconPath = path.join( process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico' ) registerProtocolHandlerWin32( @@ -100,7 +100,7 @@ function installWin32 () { */ function registerProtocolHandlerWin32 (protocol, name, icon, command) { - var protocolKey = new Registry({ + const protocolKey = new Registry({ hive: Registry.HKCU, // HKEY_CURRENT_USER key: '\\Software\\Classes\\' + protocol }) @@ -120,7 +120,7 @@ function installWin32 () { function setIcon (err) { if (err) log.error(err.message) - var iconKey = new Registry({ + const iconKey = new Registry({ hive: Registry.HKCU, key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon' }) @@ -130,7 +130,7 @@ function installWin32 () { function setCommand (err) { if (err) log.error(err.message) - var commandKey = new Registry({ + const commandKey = new Registry({ hive: Registry.HKCU, key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command' }) @@ -161,7 +161,7 @@ function installWin32 () { setExt() function setExt () { - var extKey = new Registry({ + const extKey = new Registry({ hive: Registry.HKCU, // HKEY_CURRENT_USER key: '\\Software\\Classes\\' + ext }) @@ -171,7 +171,7 @@ function installWin32 () { function setId (err) { if (err) log.error(err.message) - var idKey = new Registry({ + const idKey = new Registry({ hive: Registry.HKCU, key: '\\Software\\Classes\\' + id }) @@ -181,7 +181,7 @@ function installWin32 () { function setIcon (err) { if (err) log.error(err.message) - var iconKey = new Registry({ + const iconKey = new Registry({ hive: Registry.HKCU, key: '\\Software\\Classes\\' + id + '\\DefaultIcon' }) @@ -191,7 +191,7 @@ function installWin32 () { function setCommand (err) { if (err) log.error(err.message) - var commandKey = new Registry({ + const commandKey = new Registry({ hive: Registry.HKCU, key: '\\Software\\Classes\\' + id + '\\shell\\open\\command' }) @@ -205,7 +205,7 @@ function installWin32 () { } function uninstallWin32 () { - var Registry = require('winreg') + const Registry = require('winreg') unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND) @@ -215,7 +215,7 @@ function uninstallWin32 () { getCommand() function getCommand () { - var commandKey = new Registry({ + const commandKey = new Registry({ hive: Registry.HKCU, // HKEY_CURRENT_USER key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command' }) @@ -227,7 +227,7 @@ function uninstallWin32 () { } function destroyProtocol () { - var protocolKey = new Registry({ + const protocolKey = new Registry({ hive: Registry.HKCU, key: '\\Software\\Classes\\' + protocol }) @@ -239,7 +239,7 @@ function uninstallWin32 () { eraseId() function eraseId () { - var idKey = new Registry({ + const idKey = new Registry({ hive: Registry.HKCU, // HKEY_CURRENT_USER key: '\\Software\\Classes\\' + id }) @@ -247,7 +247,7 @@ function uninstallWin32 () { } function getExt () { - var extKey = new Registry({ + const extKey = new Registry({ hive: Registry.HKCU, key: '\\Software\\Classes\\' + ext }) @@ -259,7 +259,7 @@ function uninstallWin32 () { } function destroyExt () { - var extKey = new Registry({ + const extKey = new Registry({ hive: Registry.HKCU, // HKEY_CURRENT_USER key: '\\Software\\Classes\\' + ext }) @@ -273,12 +273,12 @@ function commandToArgs (command) { } function installLinux () { - var fs = require('fs-extra') - var os = require('os') - var path = require('path') + const fs = require('fs') + const os = require('os') + const path = require('path') - var config = require('../config') - var log = require('./log') + const config = require('../config') + const log = require('./log') // Do not install in user dir if running on system if (/^\/opt/.test(process.execPath)) return @@ -287,7 +287,7 @@ function installLinux () { installIconFile() function installDesktopFile () { - var templatePath = path.join( + const templatePath = path.join( config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop' ) fs.readFile(templatePath, 'utf8', writeDesktopFile) @@ -296,7 +296,7 @@ function installLinux () { function writeDesktopFile (err, desktopFile) { if (err) return log.error(err.message) - var appPath = config.IS_PRODUCTION + const appPath = config.IS_PRODUCTION ? path.dirname(process.execPath) : config.ROOT_PATH @@ -305,7 +305,7 @@ function installLinux () { desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' ')) desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath) - var desktopFilePath = path.join( + const desktopFilePath = path.join( os.homedir(), '.local', 'share', @@ -319,47 +319,51 @@ function installLinux () { } function installIconFile () { - var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png') + const iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png') fs.readFile(iconStaticPath, writeIconFile) } function writeIconFile (err, iconFile) { if (err) return log.error(err.message) - var iconFilePath = path.join( + const mkdirp = require('mkdirp') + + const iconFilePath = path.join( os.homedir(), '.local', 'share', 'icons', 'webtorrent-desktop.png' ) - fs.mkdirp(path.dirname(iconFilePath)) - fs.writeFile(iconFilePath, iconFile, function (err) { + mkdirp(path.dirname(iconFilePath), (err) => { if (err) return log.error(err.message) + fs.writeFile(iconFilePath, iconFile, (err) => { + if (err) log.error(err.message) + }) }) } } function uninstallLinux () { - var os = require('os') - var path = require('path') - var fs = require('fs-extra') + const os = require('os') + const path = require('path') + const rimraf = require('rimraf') - var desktopFilePath = path.join( + const desktopFilePath = path.join( os.homedir(), '.local', 'share', 'applications', 'webtorrent-desktop.desktop' ) - fs.removeSync(desktopFilePath) + rimraf(desktopFilePath) - var iconFilePath = path.join( + const iconFilePath = path.join( os.homedir(), '.local', 'share', 'icons', 'webtorrent-desktop.png' ) - fs.removeSync(iconFilePath) + rimraf(iconFilePath) } diff --git a/src/main/index.js b/src/main/index.js index 3ed6d344..38286977 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,26 +1,25 @@ console.time('init') -var electron = require('electron') +const electron = require('electron') +const app = electron.app -var app = electron.app -var ipcMain = electron.ipcMain +const parallel = require('run-parallel') -var announcement = require('./announcement') -var config = require('../config') -var crashReporter = require('../crash-reporter') -var dialog = require('./dialog') -var dock = require('./dock') -var ipc = require('./ipc') -var log = require('./log') -var menu = require('./menu') -var squirrelWin32 = require('./squirrel-win32') -var tray = require('./tray') -var updater = require('./updater') -var userTasks = require('./user-tasks') -var windows = require('./windows') +const config = require('../config') +const crashReporter = require('../crash-reporter') +const ipc = require('./ipc') +const log = require('./log') +const menu = require('./menu') +const State = require('../renderer/lib/state') +const windows = require('./windows') -var shouldQuit = false -var argv = sliceArgv(process.argv) +let shouldQuit = false +let argv = sliceArgv(process.argv) + +// Start the app without showing the main window when auto launching on login +// (On Windows and Linux, we get a flag. On MacOS, we get special API.) +const hidden = argv.includes('--hidden') || + (process.platform === 'darwin' && app.getLoginItemSettings().wasOpenedAsHidden) if (config.IS_PRODUCTION) { // When Electron is running in production mode (packaged app), then run React @@ -29,13 +28,16 @@ if (config.IS_PRODUCTION) { } if (process.platform === 'win32') { + const squirrelWin32 = require('./squirrel-win32') shouldQuit = squirrelWin32.handleEvent(argv[0]) argv = argv.filter((arg) => !arg.includes('--squirrel')) } -if (!shouldQuit) { +if (!shouldQuit && !config.IS_PORTABLE) { // Prevent multiple instances of app from running at same time. New instances - // signal this instance and quit. + // signal this instance and quit. Note: This feature creates a lock file in + // %APPDATA%\Roaming\WebTorrent so we do not do it for the Portable App since + // we want to be "silent" as well as "portable". shouldQuit = app.makeSingleInstance(onAppOpen) if (shouldQuit) { app.quit() @@ -48,27 +50,30 @@ if (!shouldQuit) { function init () { if (config.IS_PORTABLE) { + const path = require('path') + // Put all user data into the "Portable Settings" folder app.setPath('userData', config.CONFIG_PATH) + // Put Electron crash files, etc. into the "Portable Settings\Temp" folder + app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp')) } - var isReady = false // app ready, windows can be created + const ipcMain = electron.ipcMain + + let isReady = false // app ready, windows can be created app.ipcReady = false // main window has finished loading and IPC is ready app.isQuitting = false - // Open handlers must be added as early as possible - app.on('open-file', onOpen) - app.on('open-url', onOpen) + parallel({ + appReady: (cb) => app.on('ready', () => cb(null)), + state: (cb) => State.load(cb) + }, onReady) - ipc.init() + function onReady (err, results) { + if (err) throw err - app.once('will-finish-launching', function () { - crashReporter.init() - }) - - app.on('ready', function () { isReady = true - windows.main.init() + windows.main.init(results.state, {hidden: hidden}) windows.webtorrent.init() menu.init() @@ -78,9 +83,18 @@ function init () { // Report uncaught exceptions process.on('uncaughtException', (err) => { console.error(err) - var error = {message: err.message, stack: err.stack} + const error = {message: err.message, stack: err.stack} windows.main.dispatch('uncaughtError', 'main', error) }) + } + + app.on('open-file', onOpen) + app.on('open-url', onOpen) + + ipc.init() + + app.once('will-finish-launching', function () { + crashReporter.init() }) app.once('ipcReady', function () { @@ -94,12 +108,12 @@ function init () { app.isQuitting = true e.preventDefault() - windows.main.dispatch('saveState') // try to save state on exit - ipcMain.once('savedState', () => app.quit()) + windows.main.dispatch('stateSaveImmediate') // try to save state on exit + ipcMain.once('stateSaved', () => app.quit()) setTimeout(() => { console.error('Saving state took too long. Quitting.') app.quit() - }, 2000) // quit after 2 secs, at most + }, 4000) // quit after 4 secs, at most }) app.on('activate', function () { @@ -108,11 +122,25 @@ function init () { } function delayedInit () { + if (app.isQuitting) return + + const announcement = require('./announcement') + const dock = require('./dock') + const updater = require('./updater') + announcement.init() dock.init() - tray.init() updater.init() - userTasks.init() + + if (process.platform === 'win32') { + const userTasks = require('./user-tasks') + userTasks.init() + } + + if (process.platform !== 'darwin') { + const tray = require('./tray') + tray.init() + } } function onOpen (e, torrentId) { @@ -143,22 +171,38 @@ function onAppOpen (newArgv) { } } +// Remove leading args. +// Production: 1 arg, eg: /Applications/WebTorrent.app/Contents/MacOS/WebTorrent +// Development: 2 args, eg: electron . +// Test: 4 args, eg: electron -r .../mocks.js . function sliceArgv (argv) { - return argv.slice(config.IS_PRODUCTION ? 1 : 2) + return argv.slice(config.IS_PRODUCTION ? 1 + : config.IS_TEST ? 4 + : 2) } function processArgv (argv) { - var torrentIds = [] + let torrentIds = [] argv.forEach(function (arg) { - if (arg === '-n') { - dialog.openSeedDirectory() - } else if (arg === '-o') { - dialog.openTorrentFile() - } else if (arg === '-u') { - dialog.openTorrentAddress() + if (arg === '-n' || arg === '-o' || arg === '-u') { + // Critical path: Only load the 'dialog' package if it is needed + const dialog = require('./dialog') + if (arg === '-n') { + dialog.openSeedDirectory() + } else if (arg === '-o') { + dialog.openTorrentFile() + } else if (arg === '-u') { + dialog.openTorrentAddress() + } + } else if (arg === '--hidden') { + // Ignore hidden argument, already being handled } else if (arg.startsWith('-psn')) { // Ignore Mac launchd "process serial number" argument // Issue: https://github.com/feross/webtorrent-desktop/issues/214 + } else if (arg.startsWith('--')) { + // Ignore Spectron flags + } else if (arg === 'data:,') { + // Ignore weird Spectron argument } else if (arg !== '.') { // Ignore '.' argument, which gets misinterpreted as a torrent id, when a // development copy of WebTorrent is started while a production version is diff --git a/src/main/ipc.js b/src/main/ipc.js index ec6f4b1b..e6110626 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -2,27 +2,19 @@ module.exports = { init } -var electron = require('electron') +const electron = require('electron') -var app = electron.app +const app = electron.app -var dialog = require('./dialog') -var dock = require('./dock') -var handlers = require('./handlers') -var log = require('./log') -var menu = require('./menu') -var powerSaveBlocker = require('./power-save-blocker') -var shell = require('./shell') -var shortcuts = require('./shortcuts') -var externalPlayer = require('./external-player') -var windows = require('./windows') -var thumbar = require('./thumbar') +const log = require('./log') +const menu = require('./menu') +const windows = require('./windows') // Messages from the main process, to be sent once the WebTorrent process starts -var messageQueueMainToWebTorrent = [] +const messageQueueMainToWebTorrent = [] function init () { - var ipc = electron.ipcMain + const ipc = electron.ipcMain ipc.once('ipcReady', function (e) { app.ipcReady = true @@ -43,40 +35,73 @@ function init () { * Dialog */ - ipc.on('openTorrentFile', () => dialog.openTorrentFile()) - ipc.on('openFiles', () => dialog.openFiles()) + ipc.on('openTorrentFile', () => { + const dialog = require('./dialog') + dialog.openTorrentFile() + }) + ipc.on('openFiles', () => { + const dialog = require('./dialog') + dialog.openFiles() + }) /** * Dock */ - ipc.on('setBadge', (e, ...args) => dock.setBadge(...args)) - ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args)) + ipc.on('setBadge', (e, ...args) => { + const dock = require('./dock') + dock.setBadge(...args) + }) + ipc.on('downloadFinished', (e, ...args) => { + const dock = require('./dock') + dock.downloadFinished(...args) + }) /** * Events */ ipc.on('onPlayerOpen', function () { - menu.setPlayerOpen(true) + const powerSaveBlocker = require('./power-save-blocker') + const shortcuts = require('./shortcuts') + const thumbar = require('./thumbar') + + menu.togglePlaybackControls(true) powerSaveBlocker.enable() shortcuts.enable() thumbar.enable() }) + ipc.on('onPlayerUpdate', function (e, ...args) { + const thumbar = require('./thumbar') + + menu.onPlayerUpdate(...args) + thumbar.onPlayerUpdate(...args) + }) + ipc.on('onPlayerClose', function () { - menu.setPlayerOpen(false) + const powerSaveBlocker = require('./power-save-blocker') + const shortcuts = require('./shortcuts') + const thumbar = require('./thumbar') + + menu.togglePlaybackControls(false) powerSaveBlocker.disable() shortcuts.disable() thumbar.disable() }) ipc.on('onPlayerPlay', function () { + const powerSaveBlocker = require('./power-save-blocker') + const thumbar = require('./thumbar') + powerSaveBlocker.enable() thumbar.onPlayerPlay() }) ipc.on('onPlayerPause', function () { + const powerSaveBlocker = require('./power-save-blocker') + const thumbar = require('./thumbar') + powerSaveBlocker.disable() thumbar.onPlayerPause() }) @@ -85,23 +110,46 @@ function init () { * Shell */ - ipc.on('openItem', (e, ...args) => shell.openItem(...args)) - ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args)) - ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args)) + ipc.on('openItem', (e, ...args) => { + const shell = require('./shell') + shell.openItem(...args) + }) + ipc.on('showItemInFolder', (e, ...args) => { + const shell = require('./shell') + shell.showItemInFolder(...args) + }) + ipc.on('moveItemToTrash', (e, ...args) => { + const shell = require('./shell') + shell.moveItemToTrash(...args) + }) /** * File handlers */ + ipc.on('setDefaultFileHandler', (e, flag) => { + const handlers = require('./handlers') + if (flag) handlers.install() else handlers.uninstall() }) + /** + * Auto start on login + */ + + ipc.on('setStartup', (e, flag) => { + const startup = require('./startup') + + if (flag) startup.install() + else startup.uninstall() + }) + /** * Windows: Main */ - var main = windows.main + const main = windows.main ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args)) ipc.on('setBounds', (e, ...args) => main.setBounds(...args)) @@ -116,16 +164,32 @@ function init () { */ ipc.on('checkForExternalPlayer', function (e, path) { - externalPlayer.checkInstall(path, function (isInstalled) { - windows.main.send('checkForExternalPlayer', isInstalled) + const externalPlayer = require('./external-player') + + externalPlayer.checkInstall(path, function (err) { + windows.main.send('checkForExternalPlayer', !err) }) }) - ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args)) - ipc.on('quitExternalPlayer', () => externalPlayer.kill()) + ipc.on('openExternalPlayer', (e, ...args) => { + const externalPlayer = require('./external-player') + const thumbar = require('./thumbar') - // Capture all events - var oldEmit = ipc.emit + menu.togglePlaybackControls(false) + thumbar.disable() + externalPlayer.spawn(...args) + }) + + ipc.on('quitExternalPlayer', () => { + const externalPlayer = require('./external-player') + externalPlayer.kill() + }) + + /** + * Message passing + */ + + const oldEmit = ipc.emit ipc.emit = function (name, e, ...args) { // Relay messages between the main window and the WebTorrent hidden window if (name.startsWith('wt-') && !app.isQuitting) { diff --git a/src/main/log.js b/src/main/log.js index 1c6eb0c6..9510e353 100644 --- a/src/main/log.js +++ b/src/main/log.js @@ -8,10 +8,10 @@ module.exports.error = error * where they can be viewed in Developer Tools. */ -var electron = require('electron') -var windows = require('./windows') +const electron = require('electron') +const windows = require('./windows') -var app = electron.app +const app = electron.app function log (...args) { if (app.ipcReady) { diff --git a/src/main/menu.js b/src/main/menu.js index 74067fa3..b9a6f3b4 100644 --- a/src/main/menu.js +++ b/src/main/menu.js @@ -1,30 +1,31 @@ module.exports = { init, - setPlayerOpen, + togglePlaybackControls, setWindowFocus, setAllowNav, + onPlayerUpdate, onToggleAlwaysOnTop, onToggleFullScreen } -var electron = require('electron') +const electron = require('electron') -var app = electron.app +const app = electron.app -var config = require('../config') -var dialog = require('./dialog') -var shell = require('./shell') -var windows = require('./windows') +const config = require('../config') +const windows = require('./windows') -var menu +let menu = null function init () { menu = electron.Menu.buildFromTemplate(getMenuTemplate()) electron.Menu.setApplicationMenu(menu) } -function setPlayerOpen (flag) { +function togglePlaybackControls (flag) { getMenuItem('Play/Pause').enabled = flag + getMenuItem('Skip Next').enabled = flag + getMenuItem('Skip Previous').enabled = flag getMenuItem('Increase Volume').enabled = flag getMenuItem('Decrease Volume').enabled = flag getMenuItem('Step Forward').enabled = flag @@ -32,6 +33,16 @@ function setPlayerOpen (flag) { getMenuItem('Increase Speed').enabled = flag getMenuItem('Decrease Speed').enabled = flag getMenuItem('Add Subtitles File...').enabled = flag + + if (flag === false) { + getMenuItem('Skip Next').enabled = false + getMenuItem('Skip Previous').enabled = false + } +} + +function onPlayerUpdate (hasNext, hasPrevious) { + getMenuItem('Skip Next').enabled = hasNext + getMenuItem('Skip Previous').enabled = hasPrevious } function setWindowFocus (flag) { @@ -59,8 +70,8 @@ function onToggleFullScreen (flag) { } function getMenuItem (label) { - for (var i = 0; i < menu.items.length; i++) { - var menuItem = menu.items[i].submenu.items.find(function (item) { + for (let i = 0; i < menu.items.length; i++) { + const menuItem = menu.items[i].submenu.items.find(function (item) { return item.label === label }) if (menuItem) return menuItem @@ -68,7 +79,7 @@ function getMenuItem (label) { } function getMenuTemplate () { - var template = [ + const template = [ { label: 'File', submenu: [ @@ -77,17 +88,26 @@ function getMenuTemplate () { ? 'Create New Torrent...' : 'Create New Torrent from Folder...', accelerator: 'CmdOrCtrl+N', - click: () => dialog.openSeedDirectory() + click: () => { + const dialog = require('./dialog') + dialog.openSeedDirectory() + } }, { label: 'Open Torrent File...', accelerator: 'CmdOrCtrl+O', - click: () => dialog.openTorrentFile() + click: () => { + const dialog = require('./dialog') + dialog.openTorrentFile() + } }, { label: 'Open Torrent Address...', accelerator: 'CmdOrCtrl+U', - click: () => dialog.openTorrentAddress() + click: () => { + const dialog = require('./dialog') + dialog.openTorrentAddress() + } }, { type: 'separator' @@ -187,6 +207,21 @@ function getMenuTemplate () { { type: 'separator' }, + { + label: 'Skip Next', + accelerator: 'N', + click: () => windows.main.dispatch('nextTrack'), + enabled: false + }, + { + label: 'Skip Previous', + accelerator: 'P', + click: () => windows.main.dispatch('previousTrack'), + enabled: false + }, + { + type: 'separator' + }, { label: 'Increase Volume', accelerator: 'CmdOrCtrl+Up', @@ -243,31 +278,53 @@ function getMenuTemplate () { } ] }, + { + label: 'Transfers', + submenu: [ + { + label: 'Pause All', + click: () => windows.main.dispatch('pauseAllTorrents') + }, + { + label: 'Resume All', + click: () => windows.main.dispatch('resumeAllTorrents') + } + ] + }, { label: 'Help', role: 'help', submenu: [ { label: 'Learn more about ' + config.APP_NAME, - click: () => shell.openExternal(config.HOME_PAGE_URL) + click: () => { + const shell = require('./shell') + shell.openExternal(config.HOME_PAGE_URL) + } }, { label: 'Contribute on GitHub', - click: () => shell.openExternal(config.GITHUB_URL) + click: () => { + const shell = require('./shell') + shell.openExternal(config.GITHUB_URL) + } }, { type: 'separator' }, { label: 'Report an Issue...', - click: () => shell.openExternal(config.GITHUB_URL_ISSUES) + click: () => { + const shell = require('./shell') + shell.openExternal(config.GITHUB_URL_ISSUES) + } } ] } ] if (process.platform === 'darwin') { - // Add WebTorrent app menu (Mac) + // WebTorrent menu (Mac) template.unshift({ label: config.APP_NAME, submenu: [ @@ -310,8 +367,26 @@ function getMenuTemplate () { ] }) - // Add Window menu (Mac) - template.splice(5, 0, { + // Edit menu (Mac) + template[2].submenu.push( + { + type: 'separator' + }, + { + label: 'Speech', + submenu: [ + { + role: 'startspeaking' + }, + { + role: 'stopspeaking' + } + ] + } + ) + + // Window menu (Mac) + template.splice(6, 0, { role: 'window', submenu: [ { @@ -333,7 +408,10 @@ function getMenuTemplate () { // File menu (Windows, Linux) template[0].submenu.unshift({ label: 'Create New Torrent from File...', - click: () => dialog.openSeedFile() + click: () => { + const dialog = require('./dialog') + dialog.openSeedFile() + } }) // Edit menu (Windows, Linux) @@ -348,7 +426,7 @@ function getMenuTemplate () { }) // Help menu (Windows, Linux) - template[4].submenu.push( + template[5].submenu.push( { type: 'separator' }, diff --git a/src/main/power-save-blocker.js b/src/main/power-save-blocker.js index 26c85a67..9756c6f7 100644 --- a/src/main/power-save-blocker.js +++ b/src/main/power-save-blocker.js @@ -3,10 +3,10 @@ module.exports = { disable } -var electron = require('electron') -var log = require('./log') +const electron = require('electron') +const log = require('./log') -var blockId = 0 +let blockId = 0 /** * Block the system from entering low-power (sleep) mode or turning off the diff --git a/src/main/shell.js b/src/main/shell.js index 83bf381f..cd133fe6 100644 --- a/src/main/shell.js +++ b/src/main/shell.js @@ -5,8 +5,8 @@ module.exports = { moveItemToTrash } -var electron = require('electron') -var log = require('./log') +const electron = require('electron') +const log = require('./log') /** * Open the given external protocol URL in the desktop’s default manner. diff --git a/src/main/shortcuts.js b/src/main/shortcuts.js index 02bc2320..eb045cc9 100644 --- a/src/main/shortcuts.js +++ b/src/main/shortcuts.js @@ -3,8 +3,8 @@ module.exports = { enable } -var electron = require('electron') -var windows = require('./windows') +const electron = require('electron') +const windows = require('./windows') function enable () { // Register play/pause media key, available on some keyboards. @@ -12,9 +12,19 @@ function enable () { 'MediaPlayPause', () => windows.main.dispatch('playPause') ) + electron.globalShortcut.register( + 'MediaNextTrack', + () => windows.main.dispatch('nextTrack') + ) + electron.globalShortcut.register( + 'MediaPreviousTrack', + () => windows.main.dispatch('previousTrack') + ) } function disable () { // Return the media key to the OS, so other apps can use it. electron.globalShortcut.unregister('MediaPlayPause') + electron.globalShortcut.unregister('MediaNextTrack') + electron.globalShortcut.unregister('MediaPreviousTrack') } diff --git a/src/main/squirrel-win32.js b/src/main/squirrel-win32.js index 9008d948..24488bea 100644 --- a/src/main/squirrel-win32.js +++ b/src/main/squirrel-win32.js @@ -2,18 +2,18 @@ module.exports = { handleEvent } -var cp = require('child_process') -var electron = require('electron') -var fs = require('fs') -var os = require('os') -var path = require('path') +const cp = require('child_process') +const electron = require('electron') +const fs = require('fs') +const os = require('os') +const path = require('path') -var app = electron.app +const app = electron.app -var handlers = require('./handlers') +const handlers = require('./handlers') -var EXE_NAME = path.basename(process.execPath) -var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe') +const EXE_NAME = path.basename(process.execPath) +const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe') function handleEvent (cmd) { if (cmd === '--squirrel-install') { @@ -73,9 +73,9 @@ function handleEvent (cmd) { * the output from standard out. */ function spawn (command, args, cb) { - var stdout = '' - - var child + let stdout = '' + let error = null + let child = null try { child = cp.spawn(command, args) } catch (err) { @@ -90,10 +90,10 @@ function spawn (command, args, cb) { stdout += data }) - var error = null child.on('error', function (processError) { error = processError }) + child.on('close', function (code, signal) { if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}') if (error) error.stdout = stdout @@ -122,12 +122,12 @@ function createShortcuts (cb) { * command. */ function updateShortcuts (cb) { - var homeDir = os.homedir() + const homeDir = os.homedir() if (homeDir) { - var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk') + const desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk') // If the desktop shortcut was deleted by the user, then keep it deleted. fs.access(desktopShortcutPath, function (err) { - var desktopShortcutExists = !err + const desktopShortcutExists = !err createShortcuts(function () { if (desktopShortcutExists) { cb() diff --git a/src/main/startup.js b/src/main/startup.js new file mode 100644 index 00000000..443c69c8 --- /dev/null +++ b/src/main/startup.js @@ -0,0 +1,36 @@ +module.exports = { + install, + uninstall +} + +const config = require('../config') +const AutoLaunch = require('auto-launch') +const { app } = require('electron') + +// On Mac, work around a bug in auto-launch where it opens a Terminal window +// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437 +const appPath = process.platform === 'darwin' + ? app.getPath('exe').replace(/\.app\/Content.*/, '.app') + : undefined // Use the default + +const appLauncher = new AutoLaunch({ + name: config.APP_NAME, + path: appPath, + isHidden: true +}) + +function install () { + return appLauncher + .isEnabled() + .then(enabled => { + if (!enabled) return appLauncher.enable() + }) +} + +function uninstall () { + return appLauncher + .isEnabled() + .then(enabled => { + if (enabled) return appLauncher.disable() + }) +} diff --git a/src/main/thumbar.js b/src/main/thumbar.js index 749aa1a3..fd30d576 100644 --- a/src/main/thumbar.js +++ b/src/main/thumbar.js @@ -2,7 +2,8 @@ module.exports = { disable, enable, onPlayerPause, - onPlayerPlay + onPlayerPlay, + onPlayerUpdate } /** @@ -11,44 +12,80 @@ module.exports = { * or activating the window. */ -var path = require('path') -var config = require('../config') +const path = require('path') +const config = require('../config') -var windows = require('./windows') +const windows = require('./windows') + +const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png') +const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png') +const PAUSE_ICON = path.join(config.STATIC_PATH, 'PauseThumbnailBarButton.png') +const NEXT_ICON = path.join(config.STATIC_PATH, 'NextTrackThumbnailBarButton.png') + +// Array indices for each button +const PREV = 0 +const PLAY_PAUSE = 1 +const NEXT = 2 + +let buttons = [] /** * Show the Windows thumbnail toolbar buttons. */ function enable () { - update(false) + buttons = [ + { + tooltip: 'Previous Track', + icon: PREV_ICON, + click: () => windows.main.dispatch('previousTrack') + }, + { + tooltip: 'Pause', + icon: PAUSE_ICON, + click: () => windows.main.dispatch('playPause') + }, + { + tooltip: 'Next Track', + icon: NEXT_ICON, + click: () => windows.main.dispatch('nextTrack') + } + ] + update() } /** * Hide the Windows thumbnail toolbar buttons. */ function disable () { - windows.main.win.setThumbarButtons([]) + buttons = [] + update() } function onPlayerPause () { - update(true) + if (!isEnabled()) return + buttons[PLAY_PAUSE].tooltip = 'Play' + buttons[PLAY_PAUSE].icon = PLAY_ICON + update() } function onPlayerPlay () { - update(false) + if (!isEnabled()) return + buttons[PLAY_PAUSE].tooltip = 'Pause' + buttons[PLAY_PAUSE].icon = PAUSE_ICON + update() } -function update (isPaused) { - var icon = isPaused - ? 'PlayThumbnailBarButton.png' - : 'PauseThumbnailBarButton.png' +function onPlayerUpdate (state) { + if (!isEnabled()) return + buttons[PREV].flags = [ state.hasPrevious ? 'enabled' : 'disabled' ] + buttons[NEXT].flags = [ state.hasNext ? 'enabled' : 'disabled' ] + update() +} - var buttons = [ - { - tooltip: isPaused ? 'Play' : 'Pause', - icon: path.join(config.STATIC_PATH, icon), - click: () => windows.main.dispatch('playPause') - } - ] +function isEnabled () { + return buttons.length > 0 +} + +function update () { windows.main.win.setThumbarButtons(buttons) } diff --git a/src/main/tray.js b/src/main/tray.js index 7816ff4b..889d1c86 100644 --- a/src/main/tray.js +++ b/src/main/tray.js @@ -4,14 +4,14 @@ module.exports = { setWindowFocus } -var electron = require('electron') +const electron = require('electron') -var app = electron.app +const app = electron.app -var config = require('../config') -var windows = require('./windows') +const config = require('../config') +const windows = require('./windows') -var tray +let tray function init () { if (process.platform === 'linux') { @@ -36,8 +36,8 @@ function setWindowFocus (flag) { } function initLinux () { - checkLinuxTraySupport(function (supportsTray) { - if (supportsTray) createTray() + checkLinuxTraySupport(function (err) { + if (!err) createTray() }) } @@ -49,16 +49,20 @@ function initWin32 () { * Check for libappindicator1 support before creating tray icon */ function checkLinuxTraySupport (cb) { - var cp = require('child_process') + const cp = require('child_process') // Check that we're on Ubuntu (or another debian system) and that we have // libappindicator1. If WebTorrent was installed from the deb file, we should // always have it. If it was installed from the zip file, we might not. cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) { - if (err) return cb(false) + if (err) return cb(err) // Unfortunately there's no cleaner way, as far as I can tell, to check // whether a debian package is installed: - cb(stdout.endsWith('\tinstall\n')) + if (stdout.endsWith('\tinstall\n')) { + cb(null) + } else { + cb(new Error('debian package not installed')) + } }) } @@ -74,7 +78,7 @@ function createTray () { } function updateTrayMenu () { - var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate()) + const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate()) tray.setContextMenu(contextMenu) } diff --git a/src/main/updater.js b/src/main/updater.js index d21ab4e2..b6d60966 100644 --- a/src/main/updater.js +++ b/src/main/updater.js @@ -2,16 +2,17 @@ module.exports = { init } -var electron = require('electron') -var get = require('simple-get') +const electron = require('electron') +const get = require('simple-get') -var config = require('../config') -var log = require('./log') -var windows = require('./windows') +const config = require('../config') +const log = require('./log') +const windows = require('./windows') -var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL + +const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL + '?version=' + config.APP_VERSION + - '&platform=' + process.platform + '&platform=' + process.platform + + '&sysarch=' + config.OS_SYSARCH function init () { if (process.platform === 'linux') { diff --git a/src/main/user-tasks.js b/src/main/user-tasks.js index 47e63f8a..a3c6ab54 100644 --- a/src/main/user-tasks.js +++ b/src/main/user-tasks.js @@ -2,9 +2,9 @@ module.exports = { init } -var electron = require('electron') +const electron = require('electron') -var app = electron.app +const app = electron.app /** * Add a user task menu to the app icon on right-click. (Windows) diff --git a/src/main/windows/about.js b/src/main/windows/about.js index 2fbfc171..83cc5155 100644 --- a/src/main/windows/about.js +++ b/src/main/windows/about.js @@ -1,17 +1,17 @@ -var about = module.exports = { +const about = module.exports = { init, win: null } -var config = require('../../config') -var electron = require('electron') +const config = require('../../config') +const electron = require('electron') function init () { if (about.win) { return about.win.show() } - var win = about.win = new electron.BrowserWindow({ + const win = about.win = new electron.BrowserWindow({ backgroundColor: '#ECECEC', center: true, fullscreen: false, @@ -32,7 +32,7 @@ function init () { // No menu on the About window win.setMenu(null) - win.webContents.once('did-finish-load', function () { + win.once('ready-to-show', function () { win.show() }) diff --git a/src/main/windows/main.js b/src/main/windows/main.js index 4b9129d8..104ac132 100644 --- a/src/main/windows/main.js +++ b/src/main/windows/main.js @@ -1,4 +1,4 @@ -var main = module.exports = { +const main = module.exports = { dispatch, hide, init, @@ -14,43 +14,59 @@ var main = module.exports = { win: null } -var electron = require('electron') +const electron = require('electron') +const debounce = require('debounce') -var app = electron.app +const app = electron.app -var config = require('../../config') -var log = require('../log') -var menu = require('../menu') -var tray = require('../tray') +const config = require('../../config') +const log = require('../log') +const menu = require('../menu') -var HEADER_HEIGHT = 38 -var TORRENT_HEIGHT = 100 - -function init () { +function init (state, options) { if (main.win) { return main.win.show() } - var win = main.win = new electron.BrowserWindow({ + + const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds) + + const win = main.win = new electron.BrowserWindow({ backgroundColor: '#282828', + backgroundThrottling: false, // do not throttle animations/timers when page is background darkTheme: true, // Forces dark theme (GTK+3) + height: initialBounds.height, icon: getIconPath(), // Window icon (Windows, Linux) - minWidth: config.WINDOW_MIN_WIDTH, minHeight: config.WINDOW_MIN_HEIGHT, + minWidth: config.WINDOW_MIN_WIDTH, + show: false, title: config.APP_WINDOW_TITLE, titleBarStyle: 'hidden-inset', // Hide title bar (Mac) useContentSize: true, // Specify web page size without OS chrome - width: 500, - height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents + width: initialBounds.width, + x: initialBounds.x, + y: initialBounds.y }) win.loadURL(config.WINDOW_MAIN) - if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT) + win.once('ready-to-show', () => { + if (!options.hidden) win.show() + }) + + if (win.setSheetOffset) { + win.setSheetOffset(config.UI_HEADER_HEIGHT) + } win.webContents.on('dom-ready', function () { menu.onToggleFullScreen(main.win.isFullScreen()) }) + win.webContents.on('will-navigate', (e, url) => { + // Prevent drag-and-drop from navigating the Electron window, which can happen + // before our drag-and-drop handlers have been initialized. + e.preventDefault() + }) + win.on('blur', onWindowBlur) win.on('focus', onWindowFocus) @@ -69,10 +85,23 @@ function init () { win.setMenuBarVisibility(true) }) + win.on('move', debounce(function (e) { + send('windowBoundsChanged', e.sender.getBounds()) + }, 1000)) + + win.on('resize', debounce(function (e) { + send('windowBoundsChanged', e.sender.getBounds()) + }, 1000)) + win.on('close', function (e) { - if (process.platform !== 'darwin' && !tray.hasTray()) { - app.quit() - } else if (!app.isQuitting) { + if (process.platform !== 'darwin') { + const tray = require('../tray') + if (!tray.hasTray()) { + app.quit() + return + } + } + if (!app.isQuitting) { e.preventDefault() hide() } @@ -85,7 +114,7 @@ function dispatch (...args) { function hide () { if (!main.win) return - main.win.send('dispatch', 'backToList') + dispatch('backToList') main.win.hide() } @@ -114,7 +143,7 @@ function setBounds (bounds, maximize) { } // Maximize or minimize, if the second argument is present - var willBeMaximized + let willBeMaximized if (maximize === true) { if (!main.win.isMaximized()) { log('setBounds: maximizing') @@ -136,9 +165,9 @@ function setBounds (bounds, maximize) { log('setBounds: setting bounds to ' + JSON.stringify(bounds)) if (bounds.x === null && bounds.y === null) { // X and Y not specified? By default, center on current screen - var scr = electron.screen.getDisplayMatching(main.win.getBounds()) - bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2) - bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2) + const scr = electron.screen.getDisplayMatching(main.win.getBounds()) + bounds.x = Math.round(scr.bounds.x + (scr.bounds.width / 2) - (bounds.width / 2)) + bounds.y = Math.round(scr.bounds.y + (scr.bounds.height / 2) - (bounds.height / 2)) log('setBounds: centered to ' + JSON.stringify(bounds)) } // Resize the window's content area (so window border doesn't need to be taken @@ -211,12 +240,20 @@ function toggleFullScreen (flag) { function onWindowBlur () { menu.setWindowFocus(false) - tray.setWindowFocus(false) + + if (process.platform !== 'darwin') { + const tray = require('../tray') + tray.setWindowFocus(false) + } } function onWindowFocus () { menu.setWindowFocus(true) - tray.setWindowFocus(true) + + if (process.platform !== 'darwin') { + const tray = require('../tray') + tray.setWindowFocus(true) + } } function getIconPath () { diff --git a/src/main/windows/webtorrent.js b/src/main/windows/webtorrent.js index 510fa1b5..0534eb7d 100644 --- a/src/main/windows/webtorrent.js +++ b/src/main/windows/webtorrent.js @@ -1,4 +1,4 @@ -var webtorrent = module.exports = { +const webtorrent = module.exports = { init, send, show, @@ -6,14 +6,14 @@ var webtorrent = module.exports = { win: null } -var electron = require('electron') +const electron = require('electron') -var config = require('../../config') -var log = require('../log') +const config = require('../../config') function init () { - var win = webtorrent.win = new electron.BrowserWindow({ + const win = webtorrent.win = new electron.BrowserWindow({ backgroundColor: '#1E1E1E', + backgroundThrottling: false, // do not throttle animations/timers when page is background center: true, fullscreen: false, fullscreenable: false, @@ -52,7 +52,6 @@ function send (...args) { function toggleDevTools () { if (!webtorrent.win) return - log('toggleDevTools') if (webtorrent.win.webContents.isDevToolsOpened()) { webtorrent.win.webContents.closeDevTools() webtorrent.win.hide() diff --git a/src/renderer/components/header.js b/src/renderer/components/header.js index b92ebbc9..8aad88c7 100644 --- a/src/renderer/components/header.js +++ b/src/renderer/components/header.js @@ -4,9 +4,12 @@ const {dispatcher} = require('../lib/dispatcher') class Header extends React.Component { render () { - var loc = this.props.state.location + const loc = this.props.state.location return ( -
+
{this.getTitle()}
{state.window.title}
) } getAddButton () { - var state = this.props.state + const state = this.props.state if (state.location.url() !== 'home') return null return ( + {this.props.children} ) diff --git a/src/renderer/components/modal-ok-cancel.js b/src/renderer/components/modal-ok-cancel.js new file mode 100644 index 00000000..e687b2a5 --- /dev/null +++ b/src/renderer/components/modal-ok-cancel.js @@ -0,0 +1,25 @@ +const React = require('react') +const FlatButton = require('material-ui/FlatButton').default +const RaisedButton = require('material-ui/RaisedButton').default + +module.exports = class ModalOKCancel extends React.Component { + render () { + const cancelStyle = { marginRight: 10, color: 'black' } + const {cancelText, onCancel, okText, onOK} = this.props + return ( +
+ + +
+ ) + } +} diff --git a/src/renderer/components/open-torrent-address-modal.js b/src/renderer/components/open-torrent-address-modal.js index cd106bcb..d44bb382 100644 --- a/src/renderer/components/open-torrent-address-modal.js +++ b/src/renderer/components/open-torrent-address-modal.js @@ -1,32 +1,41 @@ const React = require('react') +const TextField = require('material-ui/TextField').default +const ModalOKCancel = require('./modal-ok-cancel') const {dispatch, dispatcher} = require('../lib/dispatcher') module.exports = class OpenTorrentAddressModal extends React.Component { render () { - // TODO: dcposch remove janky inline +
+ { this.torrentURL = c }} + fullWidth + onKeyDown={handleKeyDown.bind(this)} /> +
+
) } + + componentDidMount () { + this.torrentURL.input.focus() + } } -function handleKeyPress (e) { - if (e.which === 13) handleOK() /* hit Enter to submit */ +function handleKeyDown (e) { + if (e.which === 13) handleOK.call(this) /* hit Enter to submit */ } function handleOK () { dispatch('exitModal') - // TODO: dcposch use React refs instead - dispatch('addTorrent', document.querySelector('#add-torrent-url').value) + dispatch('addTorrent', this.torrentURL.input.value) } diff --git a/src/renderer/components/PathSelector.js b/src/renderer/components/path-selector.js similarity index 56% rename from src/renderer/components/PathSelector.js rename to src/renderer/components/path-selector.js index 2eb643fb..ca377c65 100644 --- a/src/renderer/components/PathSelector.js +++ b/src/renderer/components/path-selector.js @@ -7,6 +7,9 @@ const remote = electron.remote const RaisedButton = require('material-ui/RaisedButton').default const TextField = require('material-ui/TextField').default +// Lets you pick a file or directory. +// Uses the system Open File dialog. +// You can't edit the text field directly. class PathSelector extends React.Component { static get propTypes () { return { @@ -26,7 +29,7 @@ class PathSelector extends React.Component { } handleClick () { - var opts = Object.assign({ + const opts = Object.assign({ defaultPath: this.props.value, properties: [ 'openFile', 'openDirectory' ] }, this.props.dialog) @@ -43,48 +46,38 @@ class PathSelector extends React.Component { render () { const id = this.props.title.replace(' ', '-').toLowerCase() + const wrapperStyle = { + alignItems: 'center', + display: 'flex', + width: '100%' + } + const labelStyle = { + flex: '0 auto', + marginRight: 10, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + } + const textareaStyle = { + color: colors.grey50 + } + const textFieldStyle = { + flex: '1' + } + const text = this.props.displayValue || this.props.value + const buttonStyle = { + marginLeft: 10 + } + return ( -
-
+
+
{this.props.title}:
- - + +
) } diff --git a/src/renderer/components/remove-torrent-modal.js b/src/renderer/components/remove-torrent-modal.js index b1fda057..a885dd0b 100644 --- a/src/renderer/components/remove-torrent-modal.js +++ b/src/renderer/components/remove-torrent-modal.js @@ -1,22 +1,24 @@ const React = require('react') +const ModalOKCancel = require('./modal-ok-cancel') const {dispatch, dispatcher} = require('../lib/dispatcher') module.exports = class RemoveTorrentModal extends React.Component { render () { - var state = this.props.state - var message = state.modal.deleteData + const state = this.props.state + const message = state.modal.deleteData ? 'Are you sure you want to remove this torrent from the list and delete the data file?' : 'Are you sure you want to remove this torrent from the list?' - var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove' + const buttonText = state.modal.deleteData ? 'REMOVE DATA' : 'REMOVE' return (

{message}

-

- - -

+
) diff --git a/src/renderer/components/ShowMore.js b/src/renderer/components/show-more.js similarity index 81% rename from src/renderer/components/ShowMore.js rename to src/renderer/components/show-more.js index 62313d64..bb0084e1 100644 --- a/src/renderer/components/ShowMore.js +++ b/src/renderer/components/show-more.js @@ -1,6 +1,6 @@ const React = require('react') -const FlatButton = require('material-ui/FlatButton').default +const RaisedButton = require('material-ui/RaisedButton').default class ShowMore extends React.Component { static get propTypes () { @@ -39,14 +39,12 @@ class ShowMore extends React.Component { ? this.props.hideLabel : this.props.showLabel return ( -
+
{this.state.expanded ? this.props.children : null} - + label={label} />
) } diff --git a/src/renderer/components/unsupported-media-modal.js b/src/renderer/components/unsupported-media-modal.js index ad379b62..70920b97 100644 --- a/src/renderer/components/unsupported-media-modal.js +++ b/src/renderer/components/unsupported-media-modal.js @@ -1,35 +1,35 @@ const React = require('react') const electron = require('electron') -const path = require('path') +const ModalOKCancel = require('./modal-ok-cancel') const {dispatcher} = require('../lib/dispatcher') module.exports = class UnsupportedMediaModal extends React.Component { render () { - var state = this.props.state - var err = state.modal.error - var message = (err && err.getMessage) + const state = this.props.state + const err = state.modal.error + const message = (err && err.getMessage) ? err.getMessage() : err - var playerPath = state.saved.prefs.externalPlayerPath - var playerName = playerPath - ? path.basename(playerPath).split('.')[0] - : 'VLC' - var actionButton = state.modal.externalPlayerInstalled - ? () - : () - var playerMessage = state.modal.externalPlayerNotFound + const onAction = state.modal.externalPlayerInstalled + ? dispatcher('openExternalPlayer') + : () => this.onInstall() + const actionText = state.modal.externalPlayerInstalled + ? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase() + : 'INSTALL VLC' + const errorMessage = state.modal.externalPlayerNotFound ? 'Couldn\'t run external player. Please make sure it\'s installed.' : '' return (

Sorry, we can't play that file.

{message}

-

- - {actionButton} -

-

{playerMessage}

+ +

{errorMessage}

) } @@ -38,7 +38,7 @@ module.exports = class UnsupportedMediaModal extends React.Component { electron.shell.openExternal('http://www.videolan.org/vlc/') // TODO: dcposch send a dispatch rather than modifying state directly - var state = this.props.state + const state = this.props.state state.modal.externalPlayerInstalled = true // Assume they'll install it successfully } } diff --git a/src/renderer/components/update-available-modal.js b/src/renderer/components/update-available-modal.js index 49b8759b..03c84605 100644 --- a/src/renderer/components/update-available-modal.js +++ b/src/renderer/components/update-available-modal.js @@ -1,19 +1,24 @@ const React = require('react') const electron = require('electron') +const ModalOKCancel = require('./modal-ok-cancel') const {dispatch} = require('../lib/dispatcher') module.exports = class UpdateAvailableModal extends React.Component { render () { - var state = this.props.state + const state = this.props.state return (

A new version of WebTorrent is available: v{state.modal.version}

-

We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.

-

- - +

+ We have an auto-updater for Windows and Mac. + We don't have one for Linux yet, so you'll have to download the new version manually.

+
) diff --git a/src/renderer/controllers/media-controller.js b/src/renderer/controllers/media-controller.js index a3821683..b22a699c 100644 --- a/src/renderer/controllers/media-controller.js +++ b/src/renderer/controllers/media-controller.js @@ -1,6 +1,6 @@ -const electron = require('electron') - -const ipcRenderer = electron.ipcRenderer +const {ipcRenderer} = require('electron') +const telemetry = require('../lib/telemetry') +const Playlist = require('../lib/playlist') // Controls local play back: the