Compare commits

..

55 Commits

Author SHA1 Message Date
Feross Aboukhadijeh
4cbad1fccd 0.17.2 2016-10-10 22:44:08 -07:00
Feross Aboukhadijeh
f818564dd1 package.json: Add {"private": true}
To prevent accidental publishing to npm.
2016-10-10 22:41:17 -07:00
Feross Aboukhadijeh
8be690a26e v0.17.2 changelog 2016-10-10 22:37:24 -07:00
Feross Aboukhadijeh
8081d42477 Attempt to fix "TypeError: Cannot read property 'startPiece' of undefined"
It wasn't clear what was causing this error, and I couldn't reproduce
it.

This PR attempts to resolve the issue by just guarding against the
exception.
2016-10-10 22:31:33 -07:00
Feross Aboukhadijeh
b0b6069fe2 remove 'pug' from nodemon command (we don't use pug) 2016-10-06 00:37:11 -07:00
Feross Aboukhadijeh
994aed9af7 Fix "Cannot read property 'files' of null"
This PR fixes one of our number 2 top error (142 error reports today
alone):

Processes: webtorrent window, platforms: darwin linux win32, versions:
pre-0.12 0.14.0 0.17.0 0.17.1
TypeError: Cannot read property 'files' of null
    at getAudioMetadata (.../build/renderer/webtorrent.js:328:21)
    at EventEmitter.<anonymous> (.../build/renderer/webtorrent.js:84:74)
    at emitThree (events.js:116:13)
    at EventEmitter.emit (events.js:194:7)

This error is reproducible if you start webtorrent for the first time
and click the WIRED CD torrent. This causes the webtorrent process to
get a  'wt-get-audio-metadata' before 'wt-start-torrenting'.

You can reproduce it 100% of the time if you force the race condition
to show itself by slowing down the sending of the 'wt-start-torrenting'
event.

(This same error was showing for an unrelated reason in the past: #891)
2016-10-05 03:00:52 -07:00
Feross Aboukhadijeh
852fc86cbd Remove unecessary return statement 2016-10-05 03:00:52 -07:00
Feross Aboukhadijeh
8801a87a58 Throttle browser-window 'move' and 'resize' events
Fixes: https://github.com/feross/webtorrent-desktop/issues/1011
2016-10-05 03:00:42 -07:00
Feross Aboukhadijeh
1e10f0083c Windows: Fix impossible to delete Wired CS torrent 2016-10-05 03:00:24 -07:00
Feross Aboukhadijeh
bb40f0f11a Update Mac integration test for Sierra 10.12
The font changed slightly on the next version of Mac OS. Let's update
the screenshots to match, since I already updated. (@dcposch - you'll
need to update too if you want the integration tests to pass on your
machine)
2016-10-04 01:53:49 -07:00
Feross Aboukhadijeh
78d7243a08 package: Explicitly specify architectures to build for
Electron now offers a "armv7l" build on Linux. Because we were
specifying "all", we were generating an armv7l binary which was being
included in "WebTorrent-vX.X.X-linux.zip" along with the ia32 build,
which explains why it was 90MB+ in the last release.
2016-10-03 04:40:42 -07:00
Feross Aboukhadijeh
09724dddd9 0.17.1 2016-10-03 04:02:21 -07:00
Feross Aboukhadijeh
59286ff3cb style: prefix all test commands with "test-"
It's a style I follow in all my packages...

- test
- test-node
- test-browser
- ...
2016-10-03 02:59:50 -07:00
Feross Aboukhadijeh
3f79c90868 Make Electron a devDependency
I moved it from devDependencies to dependencies when we added the app
to npm. But now that that's gone, let's move it back.

Functionally, this causes no difference since electron-packager
automatically excludes `electron` and all devDependencies from the
packaged app.
2016-10-03 02:59:50 -07:00
Feross Aboukhadijeh
33c48d4dfb cp-file@4
Only change is dropped Node 0.10 and 0.12 support. Nice change because
it means we load 3 fewer dependencies.
2016-10-03 02:38:03 -07:00
Feross Aboukhadijeh
48fbcd7303 v0.17.1 changelog 2016-10-03 02:28:25 -07:00
Feross Aboukhadijeh
cdb7b6eb44 Style fixes to PR #995 based on feedback 2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
90b72523b7 perf: Only require('./user-tasks') on windows 2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
a1fd30f4f5 cache mui theme after it is generated 2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
fcae064dbb perf: ~40ms improvement: Lazy load controllers and page components 2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
5815d8efe7 Fix first run telemetry bug 2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
0be8a73621 perf: 140ms improvement: Hook into require() to modify how certain modules load 2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
76e1d7777c Prevent exception when quitting and delayedInit() gets called 2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
e10a84b294 Refactor: telemetry sending
- Fix bug where approxNumTorrents and other stats were not refreshed
when getting sent on 12 hour interval
- Lazy require modules
- Move setInterval into renderer/main.js
- Remove low-level https usage, use simple-get
2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
5ff2d893b9 perf: 90ms improvement: Defer more code in renderer, load state earlier
By deferring more code in the renderer and loading state earlier, we
improve startup time by another 90ms!

Before: 507 unique requires (1270-1280ms)
After: 506 unique requires  (1180-1190ms)
2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
02f5dbb63f perf: 60ms improvement: Replace fs-extra with mkdirp/rimraf/cp-file
In Electron apps, the cost of large modules is very real.

fs-extra is very convenient, but removing it caused 50 fewer unique
files to be required(), resultin in 60ms faster startup!

Before: 557 unique requires (1330-1340ms)
After: 507 unique requires (1270-1280ms)
2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
d4cfc32c8d re-order scripts 2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
d5820063a1 Perf: lazy-load more require() calls in main process
Went from 36 unique require calls, to 31 calls after this commit.
2016-10-03 01:24:53 -07:00
Feross Aboukhadijeh
21de048738 Remove unneeded console.logs 2016-10-03 01:24:53 -07:00
Benjamin Tan
06cdac8121 Remove weird outline from “Create Torrent” button.
The outline was caused by the wrong class being applied to the button.

Closes #989.
2016-10-02 21:35:26 -07:00
Mathias Rasmussen
f1aa1bdd59 go back to list on path missing error 2016-10-02 19:00:28 -07:00
Feross Aboukhadijeh
44f621b4de external player: Only run special .app login on darwin 2016-10-02 19:00:00 -07:00
Feross Aboukhadijeh
fd1a1f0f7e On paste: Do not handle multiple newline separated torrent ids
If the user accidentally pastes something that's not a torrent id, then
they get one "Invalid torrent Id" error for each line of the text.

Sure, this removes a "feature", but it's a pretty surprising one. When
I added it, I was being too clever, IMO.

The trim code can be removed, because that's handled in
controllers.torrentList.addTorrent().
2016-10-02 18:58:44 -07:00
Feross Aboukhadijeh
a38b5220ac Trim extra spaces off pasted magnet links
Before this change, using the "Open Torrent Address" dialog to paste a
magnet link would fail with leading or trailing spaces.

Pasting on the torrent list page has always trimmed. So this PR just
makes it consistent.
2016-10-02 18:58:44 -07:00
Feross Aboukhadijeh
428a07101a style: fix inconsistent naming
We don't use "ID" anywhere else in the codebase.
2016-10-02 18:58:44 -07:00
Feross Aboukhadijeh
f1ef9daa8f Update Electron to 1.4.2
Changelog: https://github.com/electron/electron/releases/tag/v1.4.2
2016-10-02 18:58:03 -07:00
Feross Aboukhadijeh
46d5c9c9e9 sound: Reduce volume even further 2016-10-01 19:36:06 -07:00
Feross Aboukhadijeh
ecd877551e sound: remove preloading
Sound playing is basically instant -- I was over-engineering when I
added this.
2016-10-01 19:36:06 -07:00
Feross Aboukhadijeh
76e071e965 Style: Use let/const instead of var 2016-10-01 00:50:04 +02:00
Mathias Rasmussen
5623c1024e delay external player if server is not ready 2016-09-30 01:14:32 -07:00
Feross Aboukhadijeh
1a20155f66 Disable Windows delta updates
See inline comment for explanation.
2016-09-28 09:48:51 +02:00
Benjamin Tan
9a17313a0c Remove unnecessary files after removal of npm package. 2016-09-28 09:28:31 +02:00
Feross Aboukhadijeh
b3ec61ddd8 Make Portable App also a Silent App
Fixes two portable app bugs, to make the app fully "silent", not just
"portable". This means that not only are all data files stored in the
"Portable Settings" folder, but the app should leave no trace on the
host system.

- Disable Electron's single instance mode so no lock file is created in
"%APPDATA%\Roaming\WebTorrent".

- Put Electron crash files, and other electron files into the "Portable
Settings\Temp" folder instead of "%APPDATA%\Temp".
2016-09-28 09:26:26 +02:00
Benjamin Tan
7dcddf90e9 Remove .nodemonignore file.
Adding configuration files for every tool used will clutter up the
repository, especially for a configuration as simple as this.
2016-09-28 09:03:46 +02:00
Feross Aboukhadijeh
a94fdcae61 Fix "Download path missing" for Portable App first run
Create "Portable Settings/Downloads" folder to prevent "Download path
missing" warning.
2016-09-28 04:22:05 +02:00
Feross Aboukhadijeh
f9bb83815f Switch fs.statSync to fs.accessSync, which is faster 2016-09-27 22:59:00 +02:00
Feross Aboukhadijeh
dccaf16a02 Merge pull request #973 from feross/feross/style
Pixel values should use numbers, not strings
2016-09-27 20:58:53 +02:00
Feross Aboukhadijeh
f4ee831319 Pixel values should use numbers, not strings
React converts to string and adds 'px' automatically
2016-09-27 11:56:12 -07:00
Feross Aboukhadijeh
5bcf1c6379 Merge pull request #972 from feross/f/controlst
Don't show pointer cursor on torrent list checkbox
2016-09-27 20:54:53 +02:00
Feross Aboukhadijeh
d2eb87e821 Merge pull request #974 from feross/feross/portable
Fix Windows Portable App mode
2016-09-27 20:47:07 +02:00
Feross Aboukhadijeh
be08925eb4 Fix Windows Portable App mode
Fixes: #971

This is a perfect example of putting too many statements into a
try-catch block. My bad. I was trying to keep the code simple, but it
bit us here.

This happens because we were using IS_PRODUCTION, but the order of the
consts at the top are:

const IS_PORTABLE = isPortable()
const IS_PRODUCTION = isProduction()

So we're inside of isPortable() and referring to IS_PRODUCTION before
it's defined. This should have thrown an exception, since const does
not allow use-before-define, but we're transforming to ES5 with Babel.

Also, standard could have caught this, but we can't enable the
use-before-define rule until this bug is fixed:
https://github.com/feross/standard/issues/636

Basically, a perfect storm.
2016-09-26 22:54:59 -07:00
Feross Aboukhadijeh
5698c22e75 Don't show pointer cursor on torrent list checkbox
Native apps don't have the "hand" cursor
2016-09-26 17:28:45 -07:00
DC
2114532f62 Integration tests: fix on Windows 2016-09-25 19:58:11 -07:00
DC
815ba00a6b Integration tests: fix on Mac 2016-09-25 19:43:09 -07:00
DC
bcd9f046fb No more npm publish 2016-09-25 18:24:38 -07:00
92 changed files with 617 additions and 436 deletions

View File

@@ -1,2 +0,0 @@
build/
dist/

View File

@@ -1,5 +1,31 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## 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 ## v0.17.0 - 2016-09-23
### Added ### Added

View File

@@ -65,7 +65,7 @@ $ npm test
### Run integration tests ### Run integration tests
``` ```
$ npm run integration-test $ 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? The integration tests use Spectron and Tape. They click through the app, taking screenshots and comparing each one to a reference. Why screenshots?

View File

@@ -5,13 +5,13 @@
* Useful for developers. * Useful for developers.
*/ */
var fs = require('fs') const fs = require('fs')
var os = require('os') const os = require('os')
var path = require('path') const path = require('path')
var rimraf = require('rimraf') const rimraf = require('rimraf')
var config = require('../src/config') const config = require('../src/config')
var handlers = require('../src/main/handlers') const handlers = require('../src/main/handlers')
// First, remove generated files // First, remove generated files
rimraf.sync('build/') rimraf.sync('build/')
@@ -21,7 +21,7 @@ rimraf.sync('dist/')
rimraf.sync(config.CONFIG_PATH) rimraf.sync(config.CONFIG_PATH)
// Remove any temporary files // Remove any temporary files
var tmpPath let tmpPath
try { try {
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent') tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
} catch (err) { } catch (err) {

View File

@@ -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
})

View File

@@ -7,16 +7,16 @@ const path = require('path')
let hasErrors = false let hasErrors = false
// Find all Javascript source files // Find all Javascript source files
var files = walkSync('src', {globs: ['**/*.js']}) const files = walkSync('src', {globs: ['**/*.js']})
console.log('Running extra-lint on ' + files.length + ' files...') console.log('Running extra-lint on ' + files.length + ' files...')
// Read each file, line by line // Read each file, line by line
files.forEach(function (file) { files.forEach(function (file) {
var filepath = path.join('src', file) const filepath = path.join('src', file)
var lines = fs.readFileSync(filepath, 'utf8').split('\n') const lines = fs.readFileSync(filepath, 'utf8').split('\n')
lines.forEach(function (line, i) { lines.forEach(function (line, i) {
var error let error
// Consistent JSX tag closing // Consistent JSX tag closing
if (line.match(/' {2}\/> *$/) || if (line.match(/' {2}\/> *$/) ||

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
var config = require('../src/config') const config = require('../src/config')
var open = require('open') const open = require('open')
open(config.CONFIG_PATH) open(config.CONFIG_PATH)

View File

@@ -1,28 +1,28 @@
#!/usr/bin/env node #!/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') const cp = require('child_process')
var electronPackager = require('electron-packager') const electronPackager = require('electron-packager')
var fs = require('fs') const fs = require('fs')
var minimist = require('minimist') const minimist = require('minimist')
var mkdirp = require('mkdirp') const mkdirp = require('mkdirp')
var os = require('os') const os = require('os')
var path = require('path') const path = require('path')
var rimraf = require('rimraf') const rimraf = require('rimraf')
var series = require('run-series') const series = require('run-series')
var zip = require('cross-zip') const zip = require('cross-zip')
var config = require('../src/config') const config = require('../src/config')
var pkg = require('../package.json') const pkg = require('../package.json')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION const BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
var BUILD_PATH = path.join(config.ROOT_PATH, 'build') const BUILD_PATH = path.join(config.ROOT_PATH, 'build')
var DIST_PATH = path.join(config.ROOT_PATH, 'dist') const DIST_PATH = path.join(config.ROOT_PATH, 'dist')
var argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
boolean: [ boolean: [
'sign' 'sign'
], ],
@@ -43,7 +43,7 @@ function build () {
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' }) cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
console.log('Build: Transpiled to ES5.') console.log('Build: Transpiled to ES5.')
var platform = argv._[0] const platform = argv._[0]
if (platform === 'darwin') { if (platform === 'darwin') {
buildDarwin(printDone) buildDarwin(printDone)
} else if (platform === 'win32') { } else if (platform === 'win32') {
@@ -61,7 +61,7 @@ function build () {
} }
} }
var all = { const all = {
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata // The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
// property on Windows, and `NSHumanReadableCopyright` on Mac. // property on Windows, and `NSHumanReadableCopyright` on Mac.
'app-copyright': config.APP_COPYRIGHT, 'app-copyright': config.APP_COPYRIGHT,
@@ -108,7 +108,7 @@ var all = {
version: require('electron/package.json').version version: require('electron/package.json').version
} }
var darwin = { const darwin = {
// Build for Mac // Build for Mac
platform: 'darwin', platform: 'darwin',
@@ -129,12 +129,12 @@ var darwin = {
icon: config.APP_ICON + '.icns' icon: config.APP_ICON + '.icns'
} }
var win32 = { const win32 = {
// Build for Windows. // Build for Windows.
platform: 'win32', platform: 'win32',
// Build ia32 and x64 binaries. // Build ia32 and x64 binaries.
arch: 'all', arch: ['ia32', 'x64'],
// Object hash of application metadata to embed into the executable (Windows only) // Object hash of application metadata to embed into the executable (Windows only)
win32metadata: { win32metadata: {
@@ -163,12 +163,12 @@ var win32 = {
icon: config.APP_ICON + '.ico' icon: config.APP_ICON + '.ico'
} }
var linux = { const linux = {
// Build for Linux. // Build for Linux.
platform: 'linux', platform: 'linux',
// Build ia32 and x64 binaries. // Build ia32 and x64 binaries.
arch: 'all' arch: ['ia32', 'x64']
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option. // Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
} }
@@ -176,18 +176,18 @@ var linux = {
build() build()
function buildDarwin (cb) { function buildDarwin (cb) {
var plist = require('plist') const plist = require('plist')
console.log('Mac: Packaging electron...') console.log('Mac: Packaging electron...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('Mac: Packaged electron. ' + buildPath) console.log('Mac: Packaged electron. ' + buildPath)
var appPath = path.join(buildPath[0], config.APP_NAME + '.app') const appPath = path.join(buildPath[0], config.APP_NAME + '.app')
var contentsPath = path.join(appPath, 'Contents') const contentsPath = path.join(appPath, 'Contents')
var resourcesPath = path.join(contentsPath, 'Resources') const resourcesPath = path.join(contentsPath, 'Resources')
var infoPlistPath = path.join(contentsPath, 'Info.plist') const infoPlistPath = path.join(contentsPath, 'Info.plist')
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) const infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
infoPlist.CFBundleDocumentTypes = [ infoPlist.CFBundleDocumentTypes = [
{ {
@@ -261,7 +261,7 @@ function buildDarwin (cb) {
} }
function signApp (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: * Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
@@ -276,7 +276,7 @@ function buildDarwin (cb) {
* - Xcode Command Line Tools (xcode-select --install) * - Xcode Command Line Tools (xcode-select --install)
* - Membership in the Apple Developer Program * - Membership in the Apple Developer Program
*/ */
var signOpts = { const signOpts = {
app: appPath, app: appPath,
platform: 'darwin', platform: 'darwin',
verbose: true verbose: true
@@ -302,8 +302,8 @@ function buildDarwin (cb) {
// Create .zip file (used by the auto-updater) // Create .zip file (used by the auto-updater)
console.log('Mac: Creating zip...') console.log('Mac: Creating zip...')
var inPath = path.join(buildPath[0], config.APP_NAME + '.app') const inPath = path.join(buildPath[0], config.APP_NAME + '.app')
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip') const outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
zip.zipSync(inPath, outPath) zip.zipSync(inPath, outPath)
console.log('Mac: Created zip.') console.log('Mac: Created zip.')
@@ -312,13 +312,13 @@ function buildDarwin (cb) {
function packageDmg (cb) { function packageDmg (cb) {
console.log('Mac: Creating dmg...') 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) rimraf.sync(targetPath)
// Create a .dmg (Mac disk image) file, for easy user installation. // Create a .dmg (Mac disk image) file, for easy user installation.
var dmgOpts = { const dmgOpts = {
basepath: config.ROOT_PATH, basepath: config.ROOT_PATH,
target: targetPath, target: targetPath,
specification: { specification: {
@@ -339,7 +339,7 @@ function buildDarwin (cb) {
} }
} }
var dmg = appDmg(dmgOpts) const dmg = appDmg(dmgOpts)
dmg.once('error', cb) dmg.once('error', cb)
dmg.on('progress', function (info) { dmg.on('progress', function (info) {
if (info.type === 'step-begin') console.log(info.title + '...') if (info.type === 'step-begin') console.log(info.title + '...')
@@ -353,7 +353,7 @@ function buildDarwin (cb) {
} }
function buildWin32 (cb) { function buildWin32 (cb) {
var installer = require('electron-winstaller') const installer = require('electron-winstaller')
console.log('Windows: Packaging electron...') console.log('Windows: Packaging electron...')
/* /*
@@ -361,7 +361,7 @@ function buildWin32 (cb) {
* - Windows Authenticode private key and cert (authenticode.p12) * - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt) * - Windows Authenticode password file (authenticode.txt)
*/ */
var CERT_PATH let CERT_PATH
try { try {
fs.accessSync('D:') fs.accessSync('D:')
CERT_PATH = 'D:' CERT_PATH = 'D:'
@@ -373,12 +373,12 @@ function buildWin32 (cb) {
if (err) return cb(err) if (err) return cb(err)
console.log('Windows: Packaged electron. ' + buildPath) console.log('Windows: Packaged electron. ' + buildPath)
var signWithParams let signWithParams
if (process.platform === 'win32') { if (process.platform === 'win32') {
if (argv.sign) { if (argv.sign) {
var certificateFile = path.join(CERT_PATH, 'authenticode.p12') const certificateFile = path.join(CERT_PATH, 'authenticode.p12')
var certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8') const certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
var timestampServer = 'http://timestamp.comodoca.com' const timestampServer = 'http://timestamp.comodoca.com'
signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256` signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256`
} else { } else {
printWarning() printWarning()
@@ -387,9 +387,9 @@ function buildWin32 (cb) {
printWarning() printWarning()
} }
var tasks = [] const tasks = []
buildPath.forEach(function (filesPath) { buildPath.forEach(function (filesPath) {
var destArch = filesPath.split('-').pop() const destArch = filesPath.split('-').pop()
if (argv.package === 'exe' || argv.package === 'all') { if (argv.package === 'exe' || argv.package === 'all') {
tasks.push((cb) => packageInstaller(filesPath, destArch, cb)) tasks.push((cb) => packageInstaller(filesPath, destArch, cb))
@@ -403,7 +403,7 @@ function buildWin32 (cb) {
function packageInstaller (filesPath, destArch, cb) { function packageInstaller (filesPath, destArch, cb) {
console.log(`Windows: Creating ${destArch} installer...`) console.log(`Windows: Creating ${destArch} installer...`)
var archStr = destArch === 'ia32' ? '-ia32' : '' const archStr = destArch === 'ia32' ? '-ia32' : ''
installer.createWindowsInstaller({ installer.createWindowsInstaller({
appDirectory: filesPath, appDirectory: filesPath,
@@ -423,13 +423,18 @@ function buildWin32 (cb) {
* names (i.e. RELEASES-ia32 instead of RELEASES) and so Squirrel won't * names (i.e. RELEASES-ia32 instead of RELEASES) and so Squirrel won't
* find them unless we proxy the requests. * find them unless we proxy the requests.
*/ */
remoteReleases: destArch === 'x64' // TODO: Re-enable Windows 64-bit delta updates when we confirm that they
? config.GITHUB_URL // work correctly in the presence of the "ia32" .nupkg files. I
: undefined, // (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! * If you hit a "GitHub API rate limit exceeded" error, set this token!
*/ */
remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN, // remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe', setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe',
setupIcon: config.APP_ICON + '.ico', setupIcon: config.APP_ICON + '.ico',
signWithParams: signWithParams, signWithParams: signWithParams,
@@ -454,7 +459,7 @@ function buildWin32 (cb) {
console.log('Windows: Renaming ia32 installer files...') console.log('Windows: Renaming ia32 installer files...')
// RELEASES -> RELEASES-ia32 // RELEASES -> RELEASES-ia32
var relPath = path.join(DIST_PATH, 'RELEASES-ia32') const relPath = path.join(DIST_PATH, 'RELEASES-ia32')
fs.renameSync( fs.renameSync(
path.join(DIST_PATH, 'RELEASES'), path.join(DIST_PATH, 'RELEASES'),
relPath relPath
@@ -467,8 +472,8 @@ function buildWin32 (cb) {
) )
// Change file name inside RELEASES-ia32 to match renamed file // Change file name inside RELEASES-ia32 to match renamed file
var relContent = fs.readFileSync(relPath, 'utf8') const relContent = fs.readFileSync(relPath, 'utf8')
var relContent32 = relContent.replace('full.nupkg', 'ia32-full.nupkg') const relContent32 = relContent.replace('full.nupkg', 'ia32-full.nupkg')
fs.writeFileSync(relPath, relContent32) fs.writeFileSync(relPath, relContent32)
if (relContent === relContent32) { if (relContent === relContent32) {
@@ -487,13 +492,19 @@ function buildWin32 (cb) {
function packagePortable (filesPath, destArch, cb) { function packagePortable (filesPath, destArch, cb) {
console.log(`Windows: Creating ${destArch} portable app...`) console.log(`Windows: Creating ${destArch} portable app...`)
var portablePath = path.join(filesPath, 'Portable Settings') const portablePath = path.join(filesPath, 'Portable Settings')
mkdirp.sync(portablePath) mkdirp.sync(portablePath)
var archStr = destArch === 'ia32' ? '-ia32' : '' const downloadsPath = path.join(portablePath, 'Downloads')
mkdirp.sync(downloadsPath)
var inPath = path.join(DIST_PATH, path.basename(filesPath)) const tempPath = path.join(portablePath, 'Temp')
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip') 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) zip.zipSync(inPath, outPath)
console.log(`Windows: Created ${destArch} portable app.`) console.log(`Windows: Created ${destArch} portable app.`)
@@ -508,9 +519,9 @@ function buildLinux (cb) {
if (err) return cb(err) if (err) return cb(err)
console.log('Linux: Packaged electron. ' + buildPath) console.log('Linux: Packaged electron. ' + buildPath)
var tasks = [] const tasks = []
buildPath.forEach(function (filesPath) { buildPath.forEach(function (filesPath) {
var destArch = filesPath.split('-').pop() const destArch = filesPath.split('-').pop()
if (argv.package === 'deb' || argv.package === 'all') { if (argv.package === 'deb' || argv.package === 'all') {
tasks.push((cb) => packageDeb(filesPath, destArch, cb)) tasks.push((cb) => packageDeb(filesPath, destArch, cb))
@@ -526,8 +537,8 @@ function buildLinux (cb) {
// Create .deb file for Debian-based platforms // Create .deb file for Debian-based platforms
console.log(`Linux: Creating ${destArch} deb...`) console.log(`Linux: Creating ${destArch} deb...`)
var deb = require('nobin-debian-installer')() const deb = require('nobin-debian-installer')()
var destPath = path.join('/opt', pkg.name) const destPath = path.join('/opt', pkg.name)
deb.pack({ deb.pack({
package: pkg, package: pkg,
@@ -561,10 +572,10 @@ function buildLinux (cb) {
// Create .zip file for Linux // Create .zip file for Linux
console.log(`Linux: Creating ${destArch} zip...`) console.log(`Linux: Creating ${destArch} zip...`)
var archStr = destArch === 'ia32' ? '-ia32' : '' const archStr = destArch === 'ia32' ? '-ia32' : ''
var inPath = path.join(DIST_PATH, path.basename(filesPath)) const inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux' + archStr + '.zip') const outPath = path.join(DIST_PATH, BUILD_NAME + '-linux' + archStr + '.zip')
zip.zipSync(inPath, outPath) zip.zipSync(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`) console.log(`Linux: Created ${destArch} zip.`)

View File

@@ -6,5 +6,4 @@ git diff --exit-code
npm run package -- --sign npm run package -- --sign
git push git push
git push --tags git push --tags
npm publish
npm run gh-release npm run gh-release

View File

@@ -1,15 +1,12 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.17.0", "version": "0.17.2",
"author": { "author": {
"name": "WebTorrent, LLC", "name": "WebTorrent, LLC",
"email": "feross@webtorrent.io", "email": "feross@webtorrent.io",
"url": "https://webtorrent.io" "url": "https://webtorrent.io"
}, },
"bin": {
"webtorrent-desktop": "./bin/cmd.js"
},
"bugs": { "bugs": {
"url": "https://github.com/feross/webtorrent-desktop/issues" "url": "https://github.com/feross/webtorrent-desktop/issues"
}, },
@@ -21,18 +18,20 @@
"bitfield": "^1.0.2", "bitfield": "^1.0.2",
"capture-frame": "^1.0.0", "capture-frame": "^1.0.0",
"chromecasts": "^1.8.0", "chromecasts": "^1.8.0",
"cp-file": "^4.0.1",
"create-torrent": "^3.24.5", "create-torrent": "^3.24.5",
"debounce": "^1.0.0", "debounce": "^1.0.0",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0", "dlnacasts": "^0.1.0",
"drag-drop": "^2.12.1", "drag-drop": "^2.12.1",
"electron": "1.4.1",
"es6-error": "^3.0.1", "es6-error": "^3.0.1",
"fs-extra": "^0.30.0", "fn-getter": "^1.0.0",
"iso-639-1": "^1.2.1", "iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1", "languagedetect": "^1.1.1",
"location-history": "^1.0.0", "location-history": "^1.0.0",
"lodash.merge": "^4.6.0",
"material-ui": "^0.15.4", "material-ui": "^0.15.4",
"mkdirp": "^0.5.1",
"musicmetadata": "^2.0.2", "musicmetadata": "^2.0.2",
"network-address": "^1.1.0", "network-address": "^1.1.0",
"parse-torrent": "^5.7.3", "parse-torrent": "^5.7.3",
@@ -40,6 +39,7 @@
"react": "^15.2.1", "react": "^15.2.1",
"react-dom": "^15.2.1", "react-dom": "^15.2.1",
"react-tap-event-plugin": "^1.0.0", "react-tap-event-plugin": "^1.0.0",
"rimraf": "^2.5.2",
"run-parallel": "^1.1.6", "run-parallel": "^1.1.6",
"semver": "^5.1.0", "semver": "^5.1.0",
"simple-concat": "^1.0.0", "simple-concat": "^1.0.0",
@@ -54,18 +54,17 @@
"buble": "^0.14.0", "buble": "^0.14.0",
"cross-zip": "^2.0.1", "cross-zip": "^2.0.1",
"depcheck": "^0.6.4", "depcheck": "^0.6.4",
"electron": "1.4.2",
"electron-osx-sign": "^0.3.0", "electron-osx-sign": "^0.3.0",
"electron-packager": "^8.0.0", "electron-packager": "^8.0.0",
"electron-winstaller": "^2.3.0", "electron-winstaller": "^2.3.0",
"gh-release": "^2.0.3", "gh-release": "^2.0.3",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"nobin-debian-installer": "^0.0.10", "nobin-debian-installer": "^0.0.10",
"nodemon": "^1.10.2", "nodemon": "^1.10.2",
"open": "0.0.5", "open": "0.0.5",
"plist": "^2.0.1", "plist": "^2.0.1",
"pngjs": "^3.0.0", "pngjs": "^3.0.0",
"rimraf": "^2.5.2",
"run-series": "^1.1.4", "run-series": "^1.1.4",
"spectron": "^3.3.0", "spectron": "^3.3.0",
"standard": "*", "standard": "*",
@@ -91,6 +90,7 @@
"optionalDependencies": { "optionalDependencies": {
"appdmg": "^0.4.3" "appdmg": "^0.4.3"
}, },
"private": true,
"productName": "WebTorrent", "productName": "WebTorrent",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -99,14 +99,14 @@
"scripts": { "scripts": {
"build": "buble src --output build", "build": "buble src --output build",
"clean": "node ./bin/clean.js", "clean": "node ./bin/clean.js",
"gh-release": "gh-release",
"open-config": "node ./bin/open-config.js", "open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js", "package": "node ./bin/package.js",
"prepublish": "npm run build", "prepublish": "npm run build",
"start": "npm run build && electron .", "start": "npm run build && electron .",
"integration-test": "npm run build && node ./test", "test": "standard && depcheck --ignores=buble,lodash.merge,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.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",
"gh-release": "gh-release",
"update-authors": "./bin/update-authors.sh", "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/"
} }
} }

View File

@@ -1,5 +1,4 @@
const appConfig = require('application-config')('WebTorrent') const appConfig = require('application-config')('WebTorrent')
const fs = require('fs')
const path = require('path') const path = require('path')
const electron = require('electron') const electron = require('electron')
const arch = require('arch') const arch = require('arch')
@@ -12,16 +11,12 @@ const IS_TEST = isTest()
const PORTABLE_PATH = IS_TEST const PORTABLE_PATH = IS_TEST
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest') ? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
: path.join(path.dirname(process.execPath), 'Portable Settings') : path.join(path.dirname(process.execPath), 'Portable Settings')
const IS_PORTABLE = isPortable()
const IS_PRODUCTION = isProduction() const IS_PRODUCTION = isProduction()
const IS_PORTABLE = isPortable()
const UI_HEADER_HEIGHT = 38 const UI_HEADER_HEIGHT = 38
const UI_TORRENT_HEIGHT = 100 const UI_TORRENT_HEIGHT = 100
console.log('Production: %s portable: %s test: %s',
IS_PRODUCTION, IS_PORTABLE, IS_TEST)
if (IS_PORTABLE) console.log('Portable path: %s', PORTABLE_PATH)
module.exports = { module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement', ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update', AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
@@ -65,7 +60,7 @@ module.exports = {
}, },
{ {
testID: 'wired', testID: 'wired',
name: 'The WIRED CD - Rip. Sample. Mash. Share.', name: 'The WIRED CD - Rip. Sample. Mash. Share',
posterFileName: 'wiredCd.jpg', posterFileName: 'wiredCd.jpg',
torrentFileName: 'wiredCd.torrent' torrentFileName: 'wiredCd.torrent'
} }
@@ -116,9 +111,7 @@ function getConfigPath () {
} }
function getDefaultDownloadPath () { function getDefaultDownloadPath () {
if (!process || !process.type) { if (IS_PORTABLE) {
return ''
} else if (IS_PORTABLE) {
return path.join(getConfigPath(), 'Downloads') return path.join(getConfigPath(), 'Downloads')
} else { } else {
return getPath('downloads') return getPath('downloads')
@@ -126,9 +119,14 @@ function getDefaultDownloadPath () {
} }
function getPath (key) { function getPath (key) {
if (process.type === 'renderer') { if (!process.versions.electron) {
// Node.js process
return ''
} else if (process.type === 'renderer') {
// Electron renderer process
return electron.remote.app.getPath(key) return electron.remote.app.getPath(key)
} else { } else {
// Electron main process
return electron.app.getPath(key) return electron.app.getPath(key)
} }
} }
@@ -141,8 +139,19 @@ function isPortable () {
if (IS_TEST) { if (IS_TEST) {
return true 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 { try {
return process.platform === 'win32' && IS_PRODUCTION && !!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) { } catch (err) {
return false return false
} }
@@ -150,6 +159,7 @@ function isPortable () {
function isProduction () { function isProduction () {
if (!process.versions.electron) { if (!process.versions.electron) {
// Node.js process
return false return false
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {

View File

@@ -2,10 +2,10 @@ module.exports = {
init init
} }
const config = require('./config')
const electron = require('electron')
function init () { function init () {
const config = require('./config')
const electron = require('electron')
electron.crashReporter.start({ electron.crashReporter.start({
companyName: config.APP_NAME, companyName: config.APP_NAME,
productName: config.APP_NAME, productName: config.APP_NAME,

View File

@@ -48,7 +48,7 @@ function kill () {
function spawnExternal (playerPath, args) { function spawnExternal (playerPath, args) {
log('Running external media player:', playerPath + ' ' + args.join(' ')) log('Running external media player:', playerPath + ' ' + args.join(' '))
if (path.extname(playerPath) === '.app') { if (process.platform === 'darwin' && path.extname(playerPath) === '.app') {
// Mac: Use executable in packaged .app bundle // Mac: Use executable in packaged .app bundle
playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app') playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
} }

View File

@@ -273,7 +273,7 @@ function commandToArgs (command) {
} }
function installLinux () { function installLinux () {
const fs = require('fs-extra') const fs = require('fs')
const os = require('os') const os = require('os')
const path = require('path') const path = require('path')
@@ -326,6 +326,8 @@ function installLinux () {
function writeIconFile (err, iconFile) { function writeIconFile (err, iconFile) {
if (err) return log.error(err.message) if (err) return log.error(err.message)
const mkdirp = require('mkdirp')
const iconFilePath = path.join( const iconFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
@@ -333,9 +335,11 @@ function installLinux () {
'icons', 'icons',
'webtorrent-desktop.png' 'webtorrent-desktop.png'
) )
fs.mkdirp(path.dirname(iconFilePath)) mkdirp(path.dirname(iconFilePath), (err) => {
fs.writeFile(iconFilePath, iconFile, function (err) {
if (err) return log.error(err.message) if (err) return log.error(err.message)
fs.writeFile(iconFilePath, iconFile, (err) => {
if (err) log.error(err.message)
})
}) })
} }
} }
@@ -343,7 +347,7 @@ function installLinux () {
function uninstallLinux () { function uninstallLinux () {
const os = require('os') const os = require('os')
const path = require('path') const path = require('path')
const fs = require('fs-extra') const rimraf = require('rimraf')
const desktopFilePath = path.join( const desktopFilePath = path.join(
os.homedir(), os.homedir(),
@@ -352,7 +356,7 @@ function uninstallLinux () {
'applications', 'applications',
'webtorrent-desktop.desktop' 'webtorrent-desktop.desktop'
) )
fs.removeSync(desktopFilePath) rimraf(desktopFilePath)
const iconFilePath = path.join( const iconFilePath = path.join(
os.homedir(), os.homedir(),
@@ -361,5 +365,5 @@ function uninstallLinux () {
'icons', 'icons',
'webtorrent-desktop.png' 'webtorrent-desktop.png'
) )
fs.removeSync(iconFilePath) rimraf(iconFilePath)
} }

View File

@@ -33,9 +33,11 @@ if (process.platform === 'win32') {
argv = argv.filter((arg) => !arg.includes('--squirrel')) 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 // 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) shouldQuit = app.makeSingleInstance(onAppOpen)
if (shouldQuit) { if (shouldQuit) {
app.quit() app.quit()
@@ -48,7 +50,11 @@ if (!shouldQuit) {
function init () { function init () {
if (config.IS_PORTABLE) { if (config.IS_PORTABLE) {
const path = require('path')
// Put all user data into the "Portable Settings" folder
app.setPath('userData', config.CONFIG_PATH) 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'))
} }
const ipcMain = electron.ipcMain const ipcMain = electron.ipcMain
@@ -116,17 +122,25 @@ function init () {
} }
function delayedInit () { function delayedInit () {
if (app.isQuitting) return
const announcement = require('./announcement') const announcement = require('./announcement')
const dock = require('./dock') const dock = require('./dock')
const tray = require('./tray')
const updater = require('./updater') const updater = require('./updater')
const userTasks = require('./user-tasks')
announcement.init() announcement.init()
dock.init() dock.init()
tray.init()
updater.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) { function onOpen (e, torrentId) {

View File

@@ -15,6 +15,7 @@ const main = module.exports = {
} }
const electron = require('electron') const electron = require('electron')
const debounce = require('debounce')
const app = electron.app const app = electron.app
@@ -45,18 +46,26 @@ function init (state, options) {
y: initialBounds.y y: initialBounds.y
}) })
win.once('ready-to-show', function () { win.loadURL(config.WINDOW_MAIN)
win.once('ready-to-show', () => {
if (!options.hidden) win.show() if (!options.hidden) win.show()
}) })
win.loadURL(config.WINDOW_MAIN) if (win.setSheetOffset) {
win.setSheetOffset(config.UI_HEADER_HEIGHT)
if (win.setSheetOffset) win.setSheetOffset(config.UI_HEADER_HEIGHT) }
win.webContents.on('dom-ready', function () { win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen(main.win.isFullScreen()) 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('blur', onWindowBlur)
win.on('focus', onWindowFocus) win.on('focus', onWindowFocus)
@@ -75,20 +84,23 @@ function init (state, options) {
win.setMenuBarVisibility(true) win.setMenuBarVisibility(true)
}) })
win.on('move', function (e) { win.on('move', debounce(function (e) {
send('windowBoundsChanged', e.sender.getBounds()) send('windowBoundsChanged', e.sender.getBounds())
}) }, 1000))
win.on('resize', function (e) { win.on('resize', debounce(function (e) {
send('windowBoundsChanged', e.sender.getBounds()) send('windowBoundsChanged', e.sender.getBounds())
}) }, 1000))
win.on('close', function (e) { win.on('close', function (e) {
const tray = require('../tray') if (process.platform !== 'darwin') {
const tray = require('../tray')
if (process.platform !== 'darwin' && !tray.hasTray()) { if (!tray.hasTray()) {
app.quit() app.quit()
} else if (!app.isQuitting) { return
}
}
if (!app.isQuitting) {
e.preventDefault() e.preventDefault()
hide() hide()
} }
@@ -101,7 +113,7 @@ function dispatch (...args) {
function hide () { function hide () {
if (!main.win) return if (!main.win) return
main.win.send('dispatch', 'backToList') dispatch('backToList')
main.win.hide() main.win.hide()
} }
@@ -226,17 +238,21 @@ function toggleFullScreen (flag) {
} }
function onWindowBlur () { function onWindowBlur () {
const tray = require('../tray')
menu.setWindowFocus(false) menu.setWindowFocus(false)
tray.setWindowFocus(false)
if (process.platform !== 'darwin') {
const tray = require('../tray')
tray.setWindowFocus(false)
}
} }
function onWindowFocus () { function onWindowFocus () {
const tray = require('../tray')
menu.setWindowFocus(true) menu.setWindowFocus(true)
tray.setWindowFocus(true)
if (process.platform !== 'darwin') {
const tray = require('../tray')
tray.setWindowFocus(true)
}
} }
function getIconPath () { function getIconPath () {

View File

@@ -9,7 +9,6 @@ const webtorrent = module.exports = {
const electron = require('electron') const electron = require('electron')
const config = require('../../config') const config = require('../../config')
const log = require('../log')
function init () { function init () {
const win = webtorrent.win = new electron.BrowserWindow({ const win = webtorrent.win = new electron.BrowserWindow({
@@ -52,7 +51,6 @@ function send (...args) {
function toggleDevTools () { function toggleDevTools () {
if (!webtorrent.win) return if (!webtorrent.win) return
log('toggleDevTools')
if (webtorrent.win.webContents.isDevToolsOpened()) { if (webtorrent.win.webContents.isDevToolsOpened()) {
webtorrent.win.webContents.closeDevTools() webtorrent.win.webContents.closeDevTools()
webtorrent.win.hide() webtorrent.win.hide()

View File

@@ -56,12 +56,18 @@ module.exports = class MediaController {
openExternalPlayer () { openExternalPlayer () {
const state = this.state const state = this.state
const mediaURL = Playlist.getCurrentLocalURL(this.state)
ipcRenderer.send('openExternalPlayer',
state.saved.prefs.externalPlayerPath,
mediaURL,
state.window.title)
state.playing.location = 'external' state.playing.location = 'external'
let open = function () {
const mediaURL = Playlist.getCurrentLocalURL(state)
ipcRenderer.send('openExternalPlayer',
state.saved.prefs.externalPlayerPath,
mediaURL,
state.window.title)
}
if (state.server != null) open()
else ipcRenderer.once('wt-server-running', open)
} }
externalPlayerNotFound () { externalPlayerNotFound () {

View File

@@ -268,8 +268,16 @@ module.exports = class PlaybackController {
state.playing.jumpToTime = jumpToTime state.playing.jumpToTime = jumpToTime
// if it's audio, parse out the metadata (artist, title, etc) // if it's audio, parse out the metadata (artist, title, etc)
if (state.playing.type === 'audio' && !fileSummary.audioInfo) { if (torrentSummary.status === 'paused') {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index) ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata)
} else {
getAudioMetadata()
}
function getAudioMetadata () {
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
}
} }
// if it's video, check for subtitles files that are done downloading // if it's video, check for subtitles files that are done downloading

View File

@@ -1,5 +1,5 @@
const electron = require('electron') const electron = require('electron')
const fs = require('fs-extra') const fs = require('fs')
const path = require('path') const path = require('path')
const parallel = require('run-parallel') const parallel = require('run-parallel')

View File

@@ -25,6 +25,11 @@ module.exports = class TorrentListController {
torrentId = torrentId.path torrentId = torrentId.path
} }
// Trim extra spaces off pasted magnet links
if (typeof torrentId === 'string') {
torrentId = torrentId.trim()
}
// Allow a instant.io link to be pasted // Allow a instant.io link to be pasted
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) { if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1) torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
@@ -93,6 +98,7 @@ module.exports = class TorrentListController {
fs.stat(fileOrFolder, function (err) { fs.stat(fileOrFolder, function (err) {
if (err) { if (err) {
s.error = 'path-missing' s.error = 'path-missing'
dispatch('backToList')
return return
} }
start() start()
@@ -101,7 +107,7 @@ module.exports = class TorrentListController {
function start () { function start () {
ipcRenderer.send('wt-start-torrenting', ipcRenderer.send('wt-start-torrenting',
s.torrentKey, s.torrentKey,
TorrentSummary.getTorrentID(s), TorrentSummary.getTorrentId(s),
s.path, s.path,
s.fileModtimes, s.fileModtimes,
s.selections) s.selections)

View File

@@ -29,8 +29,6 @@ class TorrentKeyNotFoundError extends TorrentError {
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) } constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
} }
class InvalidTorrentError extends TorrentError {}
module.exports = { module.exports = {
CastingError, CastingError,
PlaybackError, PlaybackError,
@@ -39,6 +37,5 @@ module.exports = {
UnplayableTorrentError, UnplayableTorrentError,
UnplayableFileError, UnplayableFileError,
InvalidSoundNameError, InvalidSoundNameError,
TorrentKeyNotFoundError, TorrentKeyNotFoundError
InvalidTorrentError
} }

View File

@@ -5,6 +5,7 @@ module.exports = {
} }
const fs = require('fs') const fs = require('fs')
const path = require('path')
const semver = require('semver') const semver = require('semver')
const config = require('../../config') const config = require('../../config')
@@ -18,37 +19,22 @@ function run (state) {
} }
const version = state.saved.version const version = state.saved.version
const saved = state.saved
if (semver.lt(version, '0.7.0')) { if (semver.lt(version, '0.7.0')) migrate_0_7_0(saved)
migrate_0_7_0(state.saved) if (semver.lt(version, '0.7.2')) migrate_0_7_2(saved)
} if (semver.lt(version, '0.11.0')) migrate_0_11_0(saved)
if (semver.lt(version, '0.12.0')) migrate_0_12_0(saved)
if (semver.lt(version, '0.7.2')) { if (semver.lt(version, '0.14.0')) migrate_0_14_0(saved)
migrate_0_7_2(state.saved) if (semver.lt(version, '0.17.0')) migrate_0_17_0(saved)
} if (semver.lt(version, '0.17.2')) migrate_0_17_2(saved)
if (semver.lt(version, '0.11.0')) {
migrate_0_11_0(state.saved)
}
if (semver.lt(version, '0.12.0')) {
migrate_0_12_0(state.saved)
}
if (semver.lt(version, '0.14.0')) {
migrate_0_14_0(state.saved)
}
if (semver.lt(version, '0.17.0')) {
migrate_0_17_0(state.saved)
}
// Config is now on the new version // Config is now on the new version
state.saved.version = config.APP_VERSION state.saved.version = config.APP_VERSION
} }
function migrate_0_7_0 (saved) { function migrate_0_7_0 (saved) {
const fs = require('fs-extra') const cpFile = require('cp-file')
const path = require('path') const path = require('path')
saved.torrents.forEach(function (ts) { saved.torrents.forEach(function (ts) {
@@ -70,7 +56,7 @@ function migrate_0_7_0 (saved) {
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent') dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
// Synchronous FS calls aren't ideal, but probably OK in a migration // Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once // that only runs once
if (src !== dst) fs.copySync(src, dst) if (src !== dst) cpFile.sync(src, dst)
delete ts.torrentPath delete ts.torrentPath
ts.torrentFileName = infoHash + '.torrent' ts.torrentFileName = infoHash + '.torrent'
@@ -85,7 +71,7 @@ function migrate_0_7_0 (saved) {
dst = path.join(config.POSTER_PATH, infoHash + extension) dst = path.join(config.POSTER_PATH, infoHash + extension)
// Synchronous FS calls aren't ideal, but probably OK in a migration // Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once // that only runs once
if (src !== dst) fs.copySync(src, dst) if (src !== dst) cpFile.sync(src, dst)
delete ts.posterURL delete ts.posterURL
ts.posterFileName = infoHash + extension ts.posterFileName = infoHash + extension
@@ -139,7 +125,7 @@ function migrate_0_12_0 (saved) {
if (!fileOrFolder) return if (!fileOrFolder) return
try { try {
fs.statSync(fileOrFolder) fs.statSync(fileOrFolder)
} catch (e) { } catch (err) {
// Default torrent with "missing path" error. Clear path. // Default torrent with "missing path" error. Clear path.
delete torrentSummary.path delete torrentSummary.path
} }
@@ -163,3 +149,60 @@ function migrate_0_17_0 (saved) {
}) })
}) })
} }
function migrate_0_17_2 (saved) {
// Remove the trailing dot (.) from the Wired CD torrent name, since
// folders/files that end in a trailing dot (.) or space are not deletable from
// Windows Explorer. See: https://github.com/feross/webtorrent-desktop/issues/905
const cpFile = require('cp-file')
const rimraf = require('rimraf')
const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.'
const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share'
const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d'
const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3'
const ts = saved.torrents.find((ts) => {
return ts.infoHash === OLD_HASH
})
if (!ts) return // Wired CD torrent does not exist
// New versions of WebTorrent ship with a fixed torrent file. Let's fix up the
// name in existing versions of WebTorrent.
ts.name = ts.displayName = NEW_NAME
ts.files.forEach((file) => {
file.path = file.path.replace(OLD_NAME, NEW_NAME)
})
// Changing the torrent name causes the info hash to change
ts.infoHash = NEW_HASH
ts.magnetURI = ts.magnetURI.replace(OLD_HASH, NEW_HASH)
try {
fs.renameSync(
path.join(config.POSTER_PATH, ts.posterFileName),
path.join(config.POSTER_PATH, NEW_HASH + '.jpg')
)
} catch (err) {}
ts.posterFileName = NEW_HASH + '.jpg'
rimraf.sync(path.join(config.TORRENT_PATH, ts.torrentFileName))
cpFile.sync(
path.join(config.STATIC_PATH, 'wiredCd.torrent'),
path.join(config.TORRENT_PATH, NEW_HASH + '.torrent')
)
ts.torrentFileName = NEW_HASH + '.torrent'
if (ts.path) {
// If torrent folder already exists on disk, try to rename it
try {
fs.renameSync(
path.join(ts.path, OLD_NAME),
path.join(ts.path, NEW_NAME)
)
} catch (err) {}
}
}

View File

@@ -72,7 +72,7 @@ function updateCache (state) {
function findPreviousIndex (state) { function findPreviousIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (var i = state.playing.fileIndex - 1; i >= 0; i--) { for (let i = state.playing.fileIndex - 1; i >= 0; i--) {
if (TorrentPlayer.isPlayable(files[i])) return i if (TorrentPlayer.isPlayable(files[i])) return i
} }
return null return null
@@ -80,7 +80,7 @@ function findPreviousIndex (state) {
function findNextIndex (state) { function findNextIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (var i = state.playing.fileIndex + 1; i < files.length; i++) { for (let i = state.playing.fileIndex + 1; i < files.length; i++) {
if (TorrentPlayer.isPlayable(files[i])) return i if (TorrentPlayer.isPlayable(files[i])) return i
} }
return null return null

View File

@@ -1,5 +1,4 @@
module.exports = { module.exports = {
preload,
play play
} }
@@ -7,7 +6,7 @@ const config = require('../../config')
const {InvalidSoundNameError} = require('./errors') const {InvalidSoundNameError} = require('./errors')
const path = require('path') const path = require('path')
const VOLUME = 0.3 const VOLUME = 0.25
/* Cache of Audio elements, for instant playback */ /* Cache of Audio elements, for instant playback */
const cache = {} const cache = {}
@@ -23,7 +22,7 @@ const sounds = {
}, },
DISABLE: { DISABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
volume: VOLUME volume: VOLUME * 0.5
}, },
DONE: { DONE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
@@ -31,7 +30,7 @@ const sounds = {
}, },
ENABLE: { ENABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
volume: VOLUME volume: VOLUME * 0.5
}, },
ERROR: { ERROR: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
@@ -39,7 +38,7 @@ const sounds = {
}, },
PLAY: { PLAY: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
volume: VOLUME * 1.25 volume: VOLUME
}, },
STARTUP: { STARTUP: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
@@ -47,17 +46,6 @@ const sounds = {
} }
} }
function preload () {
for (let name in sounds) {
if (!cache[name]) {
const sound = sounds[name]
const audio = cache[name] = new window.Audio()
audio.volume = sound.volume
audio.src = sound.url
}
}
}
function play (name) { function play (name) {
let audio = cache[name] let audio = cache[name]
if (!audio) { if (!audio) {

View File

@@ -1,23 +1,28 @@
const appConfig = require('application-config')('WebTorrent') const appConfig = require('application-config')('WebTorrent')
const debounce = require('debounce')
const path = require('path') const path = require('path')
const {EventEmitter} = require('events') const {EventEmitter} = require('events')
const config = require('../../config') const config = require('../../config')
const migrations = require('./migrations')
const SAVE_DEBOUNCE_INTERVAL = 1000 const SAVE_DEBOUNCE_INTERVAL = 1000
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
const State = module.exports = Object.assign(new EventEmitter(), { const State = module.exports = Object.assign(new EventEmitter(), {
getDefaultPlayState, getDefaultPlayState,
load, load,
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit. // state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
save: debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL), save: function () {
// Perf optimization: Lazy-require debounce (and it's dependencies)
const debounce = require('debounce')
// After first State.save() invokation, future calls go straight to the
// debounced function
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
State.save(...arguments)
},
saveImmediate saveImmediate
}) })
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
function getDefaultState () { function getDefaultState () {
const LocationHistory = require('location-history') const LocationHistory = require('location-history')
@@ -104,7 +109,8 @@ function getDefaultPlayState () {
/* If the saved state file doesn't exist yet, here's what we use instead */ /* If the saved state file doesn't exist yet, here's what we use instead */
function setupStateSaved (cb) { function setupStateSaved (cb) {
const fs = require('fs-extra') const cpFile = require('cp-file')
const fs = require('fs')
const parseTorrent = require('parse-torrent') const parseTorrent = require('parse-torrent')
const parallel = require('run-parallel') const parallel = require('run-parallel')
@@ -125,18 +131,16 @@ function setupStateSaved (cb) {
config.DEFAULT_TORRENTS.map(function (t, i) { config.DEFAULT_TORRENTS.map(function (t, i) {
const infoHash = saved.torrents[i].infoHash const infoHash = saved.torrents[i].infoHash
tasks.push(function (cb) { tasks.push(function (cb) {
fs.copy( cpFile(
path.join(config.STATIC_PATH, t.posterFileName), path.join(config.STATIC_PATH, t.posterFileName),
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)), path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
cb ).then(cb).catch(cb)
)
}) })
tasks.push(function (cb) { tasks.push(function (cb) {
fs.copy( cpFile(
path.join(config.STATIC_PATH, t.torrentFileName), path.join(config.STATIC_PATH, t.torrentFileName),
path.join(config.TORRENT_PATH, infoHash + '.torrent'), path.join(config.TORRENT_PATH, infoHash + '.torrent')
cb ).then(cb).catch(cb)
)
}) })
}) })
@@ -146,6 +150,7 @@ function setupStateSaved (cb) {
}) })
function createTorrentObject (t) { function createTorrentObject (t) {
// TODO: Doing several fs.readFileSync calls during first startup is not ideal
const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName)) const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
const parsedTorrent = parseTorrent(torrent) const parsedTorrent = parseTorrent(torrent)
@@ -205,7 +210,13 @@ function load (cb) {
if (err) return cb(err) if (err) return cb(err)
const state = getDefaultState() const state = getDefaultState()
state.saved = saved state.saved = saved
migrations.run(state)
if (process.type === 'renderer') {
// Perf optimization: Save require() calls in the main process
const migrations = require('./migrations')
migrations.run(state)
}
cb(null, state) cb(null, state)
} }
} }

View File

@@ -2,15 +2,12 @@
// Reports back so that we can improve WebTorrent Desktop // Reports back so that we can improve WebTorrent Desktop
module.exports = { module.exports = {
init, init,
send,
logUncaughtError, logUncaughtError,
logPlayAttempt logPlayAttempt
} }
const crypto = require('crypto')
const electron = require('electron') const electron = require('electron')
const https = require('https')
const os = require('os')
const url = require('url')
const config = require('../../config') const config = require('../../config')
@@ -18,11 +15,18 @@ let telemetry
function init (state) { function init (state) {
telemetry = state.saved.telemetry telemetry = state.saved.telemetry
// First app run
if (!telemetry) { if (!telemetry) {
telemetry = state.saved.telemetry = createSummary() const crypto = require('crypto')
telemetry = state.saved.telemetry = {
userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID
}
reset() reset()
} }
}
function send (state) {
const now = new Date() const now = new Date()
telemetry.version = config.APP_VERSION telemetry.version = config.APP_VERSION
telemetry.timestamp = now.toISOString() telemetry.timestamp = now.toISOString()
@@ -32,15 +36,28 @@ function init (state) {
telemetry.torrentStats = getTorrentStats(state) telemetry.torrentStats = getTorrentStats(state)
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
if (config.IS_PRODUCTION) { if (!config.IS_PRODUCTION) {
postToServer()
// If the user keeps WebTorrent running for a long time, post every 12h
setInterval(postToServer, 12 * 3600 * 1000)
} else {
// Development: telemetry used only for local debugging // Development: telemetry used only for local debugging
// Empty uncaught errors, etc at the start of every run // Empty uncaught errors, etc at the start of every run
reset() return reset()
} }
const get = require('simple-get')
const opts = {
url: config.TELEMETRY_URL,
body: telemetry,
json: true
}
get.post(opts, function (err, res) {
if (err) return console.error('Error sending telemetry', err)
if (res.statusCode !== 200) {
return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
}
console.log('Sent telemetry')
reset()
})
} }
function reset () { function reset () {
@@ -55,41 +72,6 @@ function reset () {
} }
} }
function postToServer () {
// Serialize the telemetry summary
const payload = new Buffer(JSON.stringify(telemetry), 'utf8')
// POST to our server
const options = url.parse(config.TELEMETRY_URL)
options.method = 'POST'
options.headers = {
'Content-Type': 'application/json',
'Content-Length': payload.length
}
const req = https.request(options, function (res) {
if (res.statusCode === 200) {
console.log('Successfully posted telemetry summary')
reset()
} else {
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
}
})
req.on('error', function (e) {
console.error('Couldn\'t post telemetry summary', e)
})
req.write(payload)
req.end()
}
// Creates a new telemetry summary. Gives the user a unique ID,
// collects screen resolution, etc
function createSummary () {
// Make a 256-bit random unique ID
const userID = crypto.randomBytes(32).toString('hex')
return { userID }
}
// Track screen resolution // Track screen resolution
function getScreenInfo () { function getScreenInfo () {
return electron.screen.getAllDisplays().map((screen) => ({ return electron.screen.getAllDisplays().map((screen) => ({
@@ -101,6 +83,7 @@ function getScreenInfo () {
// Track basic system info like OS version and amount of RAM // Track basic system info like OS version and amount of RAM
function getSystemInfo () { function getSystemInfo () {
const os = require('os')
return { return {
osPlatform: process.platform, osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(), osRelease: os.type() + ' ' + os.release(),
@@ -223,7 +206,7 @@ function getElemString (elem) {
let ret = elem.tagName let ret = elem.tagName
try { try {
ret += '.' + Array.from(elem.classList).join('.') ret += '.' + Array.from(elem.classList).join('.')
} catch (e) {} } catch (err) {}
return ret return ret
} }

View File

@@ -2,7 +2,7 @@ module.exports = {
getPosterPath, getPosterPath,
getTorrentPath, getTorrentPath,
getByKey, getByKey,
getTorrentID, getTorrentId,
getFileOrFolder getFileOrFolder
} }
@@ -28,7 +28,7 @@ function getPosterPath (torrentSummary) {
// Expects a torrentSummary // Expects a torrentSummary
// Returns a torrentID: filename, magnet URI, or infohash // Returns a torrentID: filename, magnet URI, or infohash
function getTorrentID (torrentSummary) { function getTorrentId (torrentSummary) {
const s = torrentSummary const s = torrentSummary
if (s.torrentFileName) { // Load torrent file from disk if (s.torrentFileName) { // Load torrent file from disk
return getTorrentPath(s) return getTorrentPath(s)

View File

@@ -1,8 +1,34 @@
/**
* Perf optimization: Hook into require() to modify how certain modules load:
*
* - `lodash/merge` (used by `material-ui`) causes 119 require() calls at startup,
* which take ~100ms. Replace it with `lodash.merge` which is equivalent.
* See: https://github.com/callemall/material-ui/pull/4380#issuecomment-250894552
*
* - `inline-style-prefixer` (used by `material-ui`) takes ~40ms. It is not
* actually used because auto-prefixing is disabled with
* `darkBaseTheme.userAgent = false`. Return a fake object.
*/
let Module = require('module')
const _require = Module.prototype.require
Module.prototype.require = function (id) {
if (id === 'lodash/merge') id = 'lodash.merge'
if (id === 'inline-style-prefixer') return {}
return _require.apply(this, arguments)
}
console.time('init') console.time('init')
const crashReporter = require('../crash-reporter') const crashReporter = require('../crash-reporter')
crashReporter.init() crashReporter.init()
// Perf optimization: Start asynchronously read on config file before all the
// blocking require() calls below.
const State = require('./lib/state')
State.load(onState)
const createGetter = require('fn-getter')
const dragDrop = require('drag-drop') const dragDrop = require('drag-drop')
const electron = require('electron') const electron = require('electron')
const fs = require('fs') const fs = require('fs')
@@ -12,22 +38,16 @@ const ReactDOM = require('react-dom')
const config = require('../config') const config = require('../config')
const telemetry = require('./lib/telemetry') const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound') const sound = require('./lib/sound')
const State = require('./lib/state')
const TorrentPlayer = require('./lib/torrent-player') const TorrentPlayer = require('./lib/torrent-player')
// Perf optimization: Needed immediately, so do not lazy load it below
const TorrentListController = require('./controllers/torrent-list-controller')
// Required by Material UI -- adds `onTouchTap` event // Required by Material UI -- adds `onTouchTap` event
require('react-tap-event-plugin')() require('react-tap-event-plugin')()
const App = require('./pages/app') const App = require('./pages/app')
const MediaController = require('./controllers/media-controller')
const UpdateController = require('./controllers/update-controller')
const PrefsController = require('./controllers/prefs-controller')
const PlaybackController = require('./controllers/playback-controller')
const SubtitlesController = require('./controllers/subtitles-controller')
const TorrentListController = require('./controllers/torrent-list-controller')
const TorrentController = require('./controllers/torrent-controller')
// Electron apps have two processes: a main process (node) runs first and starts // Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process, // a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process // and this IPC channel receives from and sends messages to the main process
@@ -50,32 +70,52 @@ let state
// Root React component // Root React component
let app let app
State.load(onState)
// Called once when the application loads. (Not once per window.) // Called once when the application loads. (Not once per window.)
// Connects to the torrent networks, sets up the UI and OS integrations like // Connects to the torrent networks, sets up the UI and OS integrations like
// the dock icon and drag+drop. // the dock icon and drag+drop.
function onState (err, _state) { function onState (err, _state) {
if (err) return onError(err) if (err) return onError(err)
state = window.state = _state // Make available for easier debugging
// Make available for easier debugging
state = window.state = _state
window.dispatch = dispatch window.dispatch = dispatch
telemetry.init(state) telemetry.init(state)
// Log uncaught JS errors // Log uncaught JS errors
window.addEventListener('error', window.addEventListener(
(e) => telemetry.logUncaughtError('window', e), 'error', (e) => telemetry.logUncaughtError('window', e), true /* capture */
true /* capture */) )
// Create controllers // Create controllers
controllers = { controllers = {
media: new MediaController(state), media: createGetter(() => {
update: new UpdateController(state), const MediaController = require('./controllers/media-controller')
prefs: new PrefsController(state, config), return new MediaController(state)
playback: new PlaybackController(state, config, update), }),
subtitles: new SubtitlesController(state), playback: createGetter(() => {
torrentList: new TorrentListController(state), const PlaybackController = require('./controllers/playback-controller')
torrent: new TorrentController(state) return new PlaybackController(state, config, update)
}),
prefs: createGetter(() => {
const PrefsController = require('./controllers/prefs-controller')
return new PrefsController(state, config)
}),
subtitles: createGetter(() => {
const SubtitlesController = require('./controllers/subtitles-controller')
return new SubtitlesController(state)
}),
torrent: createGetter(() => {
const TorrentController = require('./controllers/torrent-controller')
return new TorrentController(state)
}),
torrentList: createGetter(() => {
return new TorrentListController(state)
}),
update: createGetter(() => {
const UpdateController = require('./controllers/update-controller')
return new UpdateController(state)
})
} }
// Add first page to location history // Add first page to location history
@@ -90,23 +130,18 @@ function onState (err, _state) {
// Restart everything we were torrenting last time the app ran // Restart everything we were torrenting last time the app ran
resumeTorrents() resumeTorrents()
// Initialize ReactDOM
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
// Calling update() updates the UI given the current state // Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary // Do this at least once a second to give every file in every torrentSummary
// a progress bar and to keep the cursor in sync when playing a video // a progress bar and to keep the cursor in sync when playing a video
setInterval(update, 1000) setInterval(update, 1000)
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
window.setTimeout(delayedInit, config.DELAYED_INIT)
// Listen for messages from the main process // Listen for messages from the main process
setupIpc() setupIpc()
// Warn if the download dir is gone, eg b/c an external drive is unplugged // Drag and drop files/text to start torrenting or seeding
checkDownloadPath()
// OS integrations:
// ...drag and drop files/text to start torrenting or seeding
dragDrop('body', { dragDrop('body', {
onDrop: onOpen, onDrop: onOpen,
onDropText: onOpen onDropText: onOpen
@@ -119,20 +154,33 @@ function onState (err, _state) {
window.addEventListener('focus', onFocus) window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur) window.addEventListener('blur', onBlur)
// ...window visibility state.
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
// Done! Ideally we want to get here < 500ms after the user clicks the app
if (electron.remote.getCurrentWindow().isVisible()) { if (electron.remote.getCurrentWindow().isVisible()) {
sound.play('STARTUP') sound.play('STARTUP')
} }
// To keep app startup fast, some code is delayed.
window.setTimeout(delayedInit, config.DELAYED_INIT)
// Done! Ideally we want to get here < 500ms after the user clicks the app
console.timeEnd('init') console.timeEnd('init')
} }
// Runs a few seconds after the app loads, to avoid slowing down startup time // Runs a few seconds after the app loads, to avoid slowing down startup time
function delayedInit () { function delayedInit () {
telemetry.send(state)
// Send telemetry data every 12 hours, for users who keep the app running
// for extended periods of time
setInterval(() => telemetry.send(state), 12 * 3600 * 1000)
// Warn if the download dir is gone, eg b/c an external drive is unplugged
checkDownloadPath()
// ...window visibility state.
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
onVisibilityChange()
lazyLoadCast() lazyLoadCast()
sound.preload()
} }
// Lazily loads Chromecast and Airplay support // Lazily loads Chromecast and Airplay support
@@ -150,7 +198,7 @@ function lazyLoadCast () {
// 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller // 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller
// 4. controller - the controller handles the event, changing the state object // 4. controller - the controller handles the event, changing the state object
function update () { function update () {
controllers.playback.showOrHidePlayerControls() controllers.playback().showOrHidePlayerControls()
app.setState(state) app.setState(state)
updateElectron() updateElectron()
} }
@@ -178,54 +226,54 @@ const dispatchHandlers = {
'openFiles': () => ipcRenderer.send('openFiles'), /* shows the open file dialog */ 'openFiles': () => ipcRenderer.send('openFiles'), /* shows the open file dialog */
'openTorrentAddress': () => { state.modal = { id: 'open-torrent-address-modal' } }, 'openTorrentAddress': () => { state.modal = { id: 'open-torrent-address-modal' } },
'addTorrent': (torrentId) => controllers.torrentList.addTorrent(torrentId), 'addTorrent': (torrentId) => controllers.torrentList().addTorrent(torrentId),
'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths), 'showCreateTorrent': (paths) => controllers.torrentList().showCreateTorrent(paths),
'createTorrent': (options) => controllers.torrentList.createTorrent(options), 'createTorrent': (options) => controllers.torrentList().createTorrent(options),
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash), 'toggleTorrent': (infoHash) => controllers.torrentList().toggleTorrent(infoHash),
'toggleTorrentFile': (infoHash, index) => 'toggleTorrentFile': (infoHash, index) =>
controllers.torrentList.toggleTorrentFile(infoHash, index), controllers.torrentList().toggleTorrentFile(infoHash, index),
'confirmDeleteTorrent': (infoHash, deleteData) => 'confirmDeleteTorrent': (infoHash, deleteData) =>
controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData), controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
'deleteTorrent': (infoHash, deleteData) => 'deleteTorrent': (infoHash, deleteData) =>
controllers.torrentList.deleteTorrent(infoHash, deleteData), controllers.torrentList().deleteTorrent(infoHash, deleteData),
'toggleSelectTorrent': (infoHash) => 'toggleSelectTorrent': (infoHash) =>
controllers.torrentList.toggleSelectTorrent(infoHash), controllers.torrentList().toggleSelectTorrent(infoHash),
'openTorrentContextMenu': (infoHash) => 'openTorrentContextMenu': (infoHash) =>
controllers.torrentList.openTorrentContextMenu(infoHash), controllers.torrentList().openTorrentContextMenu(infoHash),
'startTorrentingSummary': (torrentKey) => 'startTorrentingSummary': (torrentKey) =>
controllers.torrentList.startTorrentingSummary(torrentKey), controllers.torrentList().startTorrentingSummary(torrentKey),
'saveTorrentFileAs': (torrentKey) => 'saveTorrentFileAs': (torrentKey) =>
controllers.torrentList.saveTorrentFileAs(torrentKey), controllers.torrentList().saveTorrentFileAs(torrentKey),
// Playback // Playback
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index), 'playFile': (infoHash, index) => controllers.playback().playFile(infoHash, index),
'playPause': () => controllers.playback.playPause(), 'playPause': () => controllers.playback().playPause(),
'nextTrack': () => controllers.playback.nextTrack(), 'nextTrack': () => controllers.playback().nextTrack(),
'previousTrack': () => controllers.playback.previousTrack(), 'previousTrack': () => controllers.playback().previousTrack(),
'skip': (time) => controllers.playback.skip(time), 'skip': (time) => controllers.playback().skip(time),
'skipTo': (time) => controllers.playback.skipTo(time), 'skipTo': (time) => controllers.playback().skipTo(time),
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir), 'changePlaybackRate': (dir) => controllers.playback().changePlaybackRate(dir),
'changeVolume': (delta) => controllers.playback.changeVolume(delta), 'changeVolume': (delta) => controllers.playback().changeVolume(delta),
'setVolume': (vol) => controllers.playback.setVolume(vol), 'setVolume': (vol) => controllers.playback().setVolume(vol),
'openItem': (infoHash, index) => controllers.playback.openItem(infoHash, index), 'openItem': (infoHash, index) => controllers.playback().openItem(infoHash, index),
// Subtitles // Subtitles
'openSubtitles': () => controllers.subtitles.openSubtitles(), 'openSubtitles': () => controllers.subtitles().openSubtitles(),
'selectSubtitle': (index) => controllers.subtitles.selectSubtitle(index), 'selectSubtitle': (index) => controllers.subtitles().selectSubtitle(index),
'toggleSubtitlesMenu': () => controllers.subtitles.toggleSubtitlesMenu(), 'toggleSubtitlesMenu': () => controllers.subtitles().toggleSubtitlesMenu(),
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(), 'checkForSubtitles': () => controllers.subtitles().checkForSubtitles(),
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect), 'addSubtitles': (files, autoSelect) => controllers.subtitles().addSubtitles(files, autoSelect),
// Local media: <video>, <audio>, external players // Local media: <video>, <audio>, external players
'mediaStalled': () => controllers.media.mediaStalled(), 'mediaStalled': () => controllers.media().mediaStalled(),
'mediaError': (err) => controllers.media.mediaError(err), 'mediaError': (err) => controllers.media().mediaError(err),
'mediaSuccess': () => controllers.media.mediaSuccess(), 'mediaSuccess': () => controllers.media().mediaSuccess(),
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(), 'mediaTimeUpdate': () => controllers.media().mediaTimeUpdate(),
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(), 'mediaMouseMoved': () => controllers.media().mediaMouseMoved(),
'mediaControlsMouseEnter': () => controllers.media.controlsMouseEnter(), 'mediaControlsMouseEnter': () => controllers.media().controlsMouseEnter(),
'mediaControlsMouseLeave': () => controllers.media.controlsMouseLeave(), 'mediaControlsMouseLeave': () => controllers.media().controlsMouseLeave(),
'openExternalPlayer': () => controllers.media.openExternalPlayer(), 'openExternalPlayer': () => controllers.media().openExternalPlayer(),
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(), 'externalPlayerNotFound': () => controllers.media().externalPlayerNotFound(),
// Remote casting: Chromecast, Airplay, etc // Remote casting: Chromecast, Airplay, etc
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType), 'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
@@ -233,13 +281,13 @@ const dispatchHandlers = {
'stopCasting': () => lazyLoadCast().stop(), 'stopCasting': () => lazyLoadCast().stop(),
// Preferences screen // Preferences screen
'preferences': () => controllers.prefs.show(), 'preferences': () => controllers.prefs().show(),
'updatePreferences': (key, value) => controllers.prefs.update(key, value), 'updatePreferences': (key, value) => controllers.prefs().update(key, value),
'checkDownloadPath': checkDownloadPath, 'checkDownloadPath': checkDownloadPath,
// Update (check for new versions on Linux, where there's no auto updater) // Update (check for new versions on Linux, where there's no auto updater)
'updateAvailable': (version) => controllers.update.updateAvailable(version), 'updateAvailable': (version) => controllers.update().updateAvailable(version),
'skipVersion': (version) => controllers.update.skipVersion(version), 'skipVersion': (version) => controllers.update().skipVersion(version),
// Navigation between screens (back, forward, ESC, etc) // Navigation between screens (back, forward, ESC, etc)
'exitModal': () => { state.modal = null }, 'exitModal': () => { state.modal = null },
@@ -277,7 +325,7 @@ function dispatch (action, ...args) {
// Update the virtual DOM, unless it's just a mouse move event // Update the virtual DOM, unless it's just a mouse move event
if (action !== 'mediaMouseMoved' || if (action !== 'mediaMouseMoved' ||
controllers.playback.showOrHidePlayerControls()) { controllers.playback().showOrHidePlayerControls()) {
update() update()
} }
} }
@@ -292,7 +340,7 @@ function setupIpc () {
ipcRenderer.on('fullscreenChanged', onFullscreenChanged) ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
ipcRenderer.on('windowBoundsChanged', onWindowBoundsChanged) ipcRenderer.on('windowBoundsChanged', onWindowBoundsChanged)
const tc = controllers.torrent const tc = controllers.torrent()
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args)) ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args)) ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args)) ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
@@ -345,7 +393,7 @@ function resumeTorrents () {
return torrentSummary return torrentSummary
}) })
.filter((s) => s.status !== 'paused') .filter((s) => s.status !== 'paused')
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey)) .forEach((s) => controllers.torrentList().startTorrentingSummary(s.torrentKey))
} }
// Set window dimensions to match video dimensions or fill the screen // Set window dimensions to match video dimensions or fill the screen
@@ -394,20 +442,20 @@ function onOpen (files) {
const url = state.location.url() const url = state.location.url()
const allTorrents = files.every(TorrentPlayer.isTorrent) const allTorrents = files.every(TorrentPlayer.isTorrent)
const allSubtitles = files.every(controllers.subtitles.isSubtitle) const allSubtitles = files.every(controllers.subtitles().isSubtitle)
if (allTorrents) { if (allTorrents) {
// Drop torrents onto the app: go to home screen, add torrents, no matter what // Drop torrents onto the app: go to home screen, add torrents, no matter what
dispatch('backToList') dispatch('backToList')
// All .torrent files? Add them. // All .torrent files? Add them.
files.forEach((file) => controllers.torrentList.addTorrent(file)) files.forEach((file) => controllers.torrentList().addTorrent(file))
} else if (url === 'player' && allSubtitles) { } else if (url === 'player' && allSubtitles) {
// Drop subtitles onto a playing video: add subtitles // Drop subtitles onto a playing video: add subtitles
controllers.subtitles.addSubtitles(files, true) controllers.subtitles().addSubtitles(files, true)
} else if (url === 'home') { } else if (url === 'home') {
// Drop files onto home screen: show Create Torrent // Drop files onto home screen: show Create Torrent
state.modal = null state.modal = null
controllers.torrentList.showCreateTorrent(files) controllers.torrentList().showCreateTorrent(files)
} else { } else {
// Drop files onto any other screen: show error // Drop files onto any other screen: show error
return onError('Please go back to the torrent list before creating a new torrent.') return onError('Please go back to the torrent list before creating a new torrent.')
@@ -429,13 +477,7 @@ function onError (err) {
function onPaste (e) { function onPaste (e) {
if (e.target.tagName.toLowerCase() === 'input') return if (e.target.tagName.toLowerCase() === 'input') return
controllers.torrentList().addTorrent(electron.clipboard.readText())
const torrentIds = electron.clipboard.readText().split('\n')
torrentIds.forEach(function (torrentId) {
torrentId = torrentId.trim()
if (torrentId.length === 0) return
controllers.torrentList.addTorrent(torrentId)
})
update() update()
} }

View File

@@ -1,32 +1,38 @@
const colors = require('material-ui/styles/colors') const colors = require('material-ui/styles/colors')
const createGetter = require('fn-getter')
const React = require('react') const React = require('react')
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
const getMuiTheme = require('material-ui/styles/getMuiTheme').default const getMuiTheme = require('material-ui/styles/getMuiTheme').default
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
const Header = require('../components/header') const Header = require('../components/header')
// Perf optimization: Needed immediately, so do not lazy load it below
const TorrentListPage = require('./torrent-list-page')
const Views = { const Views = {
'home': require('./torrent-list-page'), 'home': createGetter(() => TorrentListPage),
'player': require('./player-page'), 'player': createGetter(() => require('./player-page')),
'create-torrent': require('./create-torrent-page'), 'create-torrent': createGetter(() => require('./create-torrent-page')),
'preferences': require('./preferences-page') 'preferences': createGetter(() => require('./preferences-page'))
} }
const Modals = { const Modals = {
'open-torrent-address-modal': require('../components/open-torrent-address-modal'), 'open-torrent-address-modal': createGetter(
'remove-torrent-modal': require('../components/remove-torrent-modal'), () => require('../components/open-torrent-address-modal')
'update-available-modal': require('../components/update-available-modal'), ),
'unsupported-media-modal': require('../components/unsupported-media-modal') 'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')),
'update-available-modal': createGetter(() => require('../components/update-available-modal')),
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
} }
const fontFamily = process.platform === 'win32' const fontFamily = process.platform === 'win32'
? '"Segoe UI", sans-serif' ? '"Segoe UI", sans-serif'
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif' : 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
lightBaseTheme.fontFamily = fontFamily
darkBaseTheme.fontFamily = fontFamily darkBaseTheme.fontFamily = fontFamily
darkBaseTheme.userAgent = false
darkBaseTheme.palette.primary1Color = colors.grey50 darkBaseTheme.palette.primary1Color = colors.grey50
darkBaseTheme.palette.primary2Color = colors.grey50 darkBaseTheme.palette.primary2Color = colors.grey50
darkBaseTheme.palette.primary3Color = colors.grey600 darkBaseTheme.palette.primary3Color = colors.grey600
@@ -34,6 +40,9 @@ darkBaseTheme.palette.accent1Color = colors.redA200
darkBaseTheme.palette.accent2Color = colors.redA400 darkBaseTheme.palette.accent2Color = colors.redA400
darkBaseTheme.palette.accent3Color = colors.redA100 darkBaseTheme.palette.accent3Color = colors.redA100
let darkMuiTheme
let lightMuiTheme
class App extends React.Component { class App extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
@@ -53,8 +62,12 @@ class App extends React.Component {
if (state.window.isFocused) cls.push('is-focused') if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls') if (hideControls) cls.push('hide-video-controls')
const vdom = ( if (!darkMuiTheme) {
<MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}> darkMuiTheme = getMuiTheme(darkBaseTheme)
}
return (
<MuiThemeProvider muiTheme={darkMuiTheme}>
<div className={'app ' + cls.join(' ')}> <div className={'app ' + cls.join(' ')}>
<Header state={state} /> <Header state={state} />
{this.getErrorPopover()} {this.getErrorPopover()}
@@ -63,8 +76,6 @@ class App extends React.Component {
</div> </div>
</MuiThemeProvider> </MuiThemeProvider>
) )
return vdom
} }
getErrorPopover () { getErrorPopover () {
@@ -88,9 +99,17 @@ class App extends React.Component {
getModal () { getModal () {
const state = this.props.state const state = this.props.state
if (!state.modal) return if (!state.modal) return
const ModalContents = Modals[state.modal.id]
if (!lightMuiTheme) {
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
lightBaseTheme.fontFamily = fontFamily
lightBaseTheme.userAgent = false
lightMuiTheme = getMuiTheme(lightBaseTheme)
}
const ModalContents = Modals[state.modal.id]()
return ( return (
<MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}> <MuiThemeProvider muiTheme={lightMuiTheme}>
<div key='modal' className='modal'> <div key='modal' className='modal'>
<div key='modal-background' className='modal-background' /> <div key='modal-background' className='modal-background' />
<div key='modal-content' className='modal-content'> <div key='modal-content' className='modal-content'>
@@ -103,7 +122,7 @@ class App extends React.Component {
getView () { getView () {
const state = this.props.state const state = this.props.state
const View = Views[state.location.url()] const View = Views[state.location.url()]()
return (<View state={state} />) return (<View state={state} />)
} }
} }

View File

@@ -106,7 +106,7 @@ class CreateTorrentPage extends React.Component {
}} }}
onClick={dispatcher('cancel')} /> onClick={dispatcher('cancel')} />
<RaisedButton <RaisedButton
className='control create-torrent' className='control create-torrent-button'
label='Create Torrent' label='Create Torrent'
primary primary
onClick={this.handleSubmit} /> onClick={this.handleSubmit} />

View File

@@ -599,12 +599,15 @@ function renderLoadingBar (state) {
const torrentSummary = state.getPlayingTorrentSummary() const torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary.progress) { if (!torrentSummary.progress) {
return [] return null
} }
// Find all contiguous parts of the torrent which are loaded // Find all contiguous parts of the torrent which are loaded
const prog = torrentSummary.progress const prog = torrentSummary.progress
const fileProg = prog.files[state.playing.fileIndex] const fileProg = prog.files[state.playing.fileIndex]
if (!fileProg) return null
const parts = [] const parts = []
let lastPiecePresent = false let lastPiecePresent = false
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) { for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
@@ -626,6 +629,7 @@ function renderLoadingBar (state) {
return (<div key={i} className='loading-bar-part' style={style} />) return (<div key={i} className='loading-bar-part' style={style} />)
}) })
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>) return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
} }

View File

@@ -1,12 +1,12 @@
const React = require('react') const React = require('react')
const prettyBytes = require('prettier-bytes') const prettyBytes = require('prettier-bytes')
const Checkbox = require('material-ui/Checkbox').default const Checkbox = require('material-ui/Checkbox').default
const LinearProgress = require('material-ui/LinearProgress').default const LinearProgress = require('material-ui/LinearProgress').default
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player') const TorrentPlayer = require('../lib/torrent-player')
const {dispatcher} = require('../lib/dispatcher') const {dispatcher} = require('../lib/dispatcher')
const {InvalidTorrentError} = require('../lib/errors')
module.exports = class TorrentList extends React.Component { module.exports = class TorrentList extends React.Component {
render () { render () {
@@ -59,7 +59,7 @@ module.exports = class TorrentList extends React.Component {
const classes = ['torrent'] const classes = ['torrent']
if (isSelected) classes.push('selected') if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled') if (!infoHash) classes.push('disabled')
if (!torrentSummary.torrentKey) throw new InvalidTorrentError('Missing torrentKey') if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
return ( return (
<div <div
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)} id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
@@ -119,9 +119,15 @@ module.exports = class TorrentList extends React.Component {
return ( return (
<Checkbox <Checkbox
key='download-button' key='download-button'
className={'download ' + torrentSummary.status} className={'control download ' + torrentSummary.status}
style={{display: 'inline-block', width: '32px'}} style={{
iconStyle={{width: '20px', height: '20px'}} display: 'inline-block',
width: 32
}}
iconStyle={{
width: 20,
height: 20
}}
checked={isActive} checked={isActive}
onClick={stopPropagation} onClick={stopPropagation}
onCheck={dispatcher('toggleTorrent', infoHash)} /> onCheck={dispatcher('toggleTorrent', infoHash)} />
@@ -133,11 +139,11 @@ module.exports = class TorrentList extends React.Component {
const styles = { const styles = {
wrapper: { wrapper: {
display: 'inline-block', display: 'inline-block',
marginRight: '8px' marginRight: 8
}, },
progress: { progress: {
height: '8px', height: 8,
width: '30px' width: 30
} }
} }
return ( return (

View File

@@ -6,7 +6,8 @@ const crypto = require('crypto')
const deepEqual = require('deep-equal') const deepEqual = require('deep-equal')
const defaultAnnounceList = require('create-torrent').announceList const defaultAnnounceList = require('create-torrent').announceList
const electron = require('electron') const electron = require('electron')
const fs = require('fs-extra') const fs = require('fs')
const mkdirp = require('mkdirp')
const musicmetadata = require('musicmetadata') const musicmetadata = require('musicmetadata')
const networkAddress = require('network-address') const networkAddress = require('network-address')
const path = require('path') const path = require('path')
@@ -124,8 +125,6 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
// Only download the files the user wants, not necessarily all files // Only download the files the user wants, not necessarily all files
torrent.once('ready', () => selectFiles(torrent, selections)) torrent.once('ready', () => selectFiles(torrent, selections))
return torrent
} }
function stopTorrenting (infoHash) { function stopTorrenting (infoHash) {
@@ -203,19 +202,22 @@ function getTorrentFileInfo (file) {
} }
} }
// Every time we resolve a magnet URI, save the torrent file so that we never // Every time we resolve a magnet URI, save the torrent file so that we can use
// have to download it again. Never ask the DHT the same question twice. // it on next startup. Starting with the full torrent metadata will be faster
// than re-fetching it from peers using ut_metadata.
function saveTorrentFile (torrentKey) { function saveTorrentFile (torrentKey) {
const torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) { const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
fs.access(torrentPath, fs.constants.R_OK, function (err) {
const fileName = torrent.infoHash + '.torrent' const fileName = torrent.infoHash + '.torrent'
if (exists) { if (!err) {
// We've already saved the file // We've already saved the file
return ipc.send('wt-file-saved', torrentKey, fileName) return ipc.send('wt-file-saved', torrentKey, fileName)
} }
// Otherwise, save the .torrent file, under the app config folder // Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.TORRENT_PATH, function (_) { mkdirp(config.TORRENT_PATH, function (_) {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) { fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err) if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath) console.log('saved torrent file %s', torrentPath)
@@ -225,15 +227,6 @@ function saveTorrentFile (torrentKey) {
}) })
} }
// Checks whether we've already resolved a given infohash to a torrent file
// Calls back with (torrentPath, exists). Logs, does not call back on error
function checkIfTorrentFileExists (infoHash, cb) {
const torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
fs.exists(torrentPath, function (exists) {
cb(torrentPath, exists)
})
}
// Save a JPG that represents a torrent. // Save a JPG that represents a torrent.
// Auto chooses either a frame from a video file, an image, etc // Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) { function generateTorrentPoster (torrentKey) {
@@ -241,7 +234,7 @@ function generateTorrentPoster (torrentKey) {
torrentPoster(torrent, function (err, buf, extension) { torrentPoster(torrent, function (err, buf, extension) {
if (err) return console.log('error generating poster: %o', err) if (err) return console.log('error generating poster: %o', err)
// save it for next time // save it for next time
fs.mkdirp(config.POSTER_PATH, function (err) { mkdirp(config.POSTER_PATH, function (err) {
if (err) return console.log('error creating poster dir: %o', err) if (err) return console.log('error creating poster dir: %o', err)
const posterFileName = torrent.infoHash + extension const posterFileName = torrent.infoHash + extension
const posterFilePath = path.join(config.POSTER_PATH, posterFileName) const posterFilePath = path.join(config.POSTER_PATH, posterFileName)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1023 KiB

After

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 KiB

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 KiB

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 KiB

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 KiB

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 777 KiB

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1024 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1016 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 KiB

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 331 KiB

View File

@@ -1,8 +1,12 @@
const path = require('path')
const Application = require('spectron').Application const Application = require('spectron').Application
const fs = require('fs-extra') const cpFile = require('cp-file')
const fs = require('fs')
const mkdirp = require('mkdirp')
const parseTorrent = require('parse-torrent') const parseTorrent = require('parse-torrent')
const path = require('path')
const PNG = require('pngjs').PNG const PNG = require('pngjs').PNG
const rimraf = require('rimraf')
const config = require('./config') const config = require('./config')
module.exports = { module.exports = {
@@ -75,8 +79,13 @@ function endTest (app, t, err) {
function screenshotCreateOrCompare (app, t, name) { function screenshotCreateOrCompare (app, t, name) {
const ssDir = path.join(__dirname, 'screenshots', process.platform) const ssDir = path.join(__dirname, 'screenshots', process.platform)
const ssPath = path.join(ssDir, name + '.png') const ssPath = path.join(ssDir, name + '.png')
fs.ensureFileSync(ssPath) let ssBuf
const ssBuf = fs.readFileSync(ssPath)
try {
ssBuf = fs.readFileSync(ssPath)
} catch (err) {
ssBuf = Buffer.alloc(0)
}
return wait().then(function () { return wait().then(function () {
return app.browserWindow.capturePage() return app.browserWindow.capturePage()
}).then(function (buffer) { }).then(function (buffer) {
@@ -131,19 +140,19 @@ function compareIgnoringTransparency (bufActual, bufExpected) {
const rms = Math.sqrt(sumSquareDiff / (numDiff + 1)) const rms = Math.sqrt(sumSquareDiff / (numDiff + 1))
const l2Distance = Math.round(Math.sqrt(sumSquareDiff)) const l2Distance = Math.round(Math.sqrt(sumSquareDiff))
console.log('screenshot diff l2 distance: ' + l2Distance + ', rms: ' + rms) console.log('screenshot diff l2 distance: ' + l2Distance + ', rms: ' + rms)
return l2Distance < 4000 && rms < 100 return l2Distance < 5000 && rms < 100
} }
// Resets the test directory, containing config.json, torrents, downloads, etc // Resets the test directory, containing config.json, torrents, downloads, etc
function resetTestDataDir () { function resetTestDataDir () {
fs.removeSync(config.TEST_DIR) rimraf.sync(config.TEST_DIR)
// Create TEST_DIR as well as /Downloads and /Desktop // Create TEST_DIR as well as /Downloads and /Desktop
fs.mkdirpSync(config.TEST_DIR_DOWNLOAD) mkdirp.sync(config.TEST_DIR_DOWNLOAD)
fs.mkdirpSync(config.TEST_DIR_DESKTOP) mkdirp.sync(config.TEST_DIR_DESKTOP)
} }
function deleteTestDataDir () { function deleteTestDataDir () {
fs.removeSync(config.TEST_DIR) rimraf.sync(config.TEST_DIR)
} }
// Checks a given folder under Downloads. // Checks a given folder under Downloads.
@@ -159,11 +168,11 @@ function compareDownloadFolder (t, dirname, filenames) {
const expectedSorted = filenames.slice().sort() const expectedSorted = filenames.slice().sort()
const actualSorted = actualFilenames.slice().sort() const actualSorted = actualFilenames.slice().sort()
t.deepEqual(actualSorted, expectedSorted, 'download folder contents: ' + dirname) t.deepEqual(actualSorted, expectedSorted, 'download folder contents: ' + dirname)
} catch (e) { } catch (err) {
if (e.code === 'ENOENT') { if (err.code === 'ENOENT') {
t.equal(filenames, null, 'download folder missing: ' + dirname) t.equal(filenames, null, 'download folder missing: ' + dirname)
} else { } else {
console.error(e) console.error(err)
t.fail('unexpected error getting download folder: ' + dirname) t.fail('unexpected error getting download folder: ' + dirname)
} }
} }
@@ -201,13 +210,12 @@ function extractImportantFields (parsedTorrent) {
function copy (pathFrom, pathTo) { function copy (pathFrom, pathTo) {
try { try {
fs.copySync(pathFrom, pathTo) cpFile.sync(pathFrom, pathTo)
} catch (e) { } catch (err) {
// There is a bug in either node or `fs-extra` on windows
// Windows lets us create files and folders under C:\Windows\Temp, // Windows lets us create files and folders under C:\Windows\Temp,
// but when you try to `copySync` into one of those folders, you get EPERM // but when you try to `copySync` into one of those folders, you get EPERM
// Ignore for now... // Ignore for now...
if (process.platform !== 'win32' || e.code !== 'EPERM') throw e if (process.platform !== 'win32' || err.code !== 'EPERM') throw err
console.log('ignoring windows copy EPERM error', e) console.log('ignoring windows copy EPERM error', err)
} }
} }

View File

@@ -70,7 +70,7 @@ test('create-torrent', function (t) {
.then(() => app.client.waitUntilTextExists('.create-torrent-advanced', 'Comment')) .then(() => app.client.waitUntilTextExists('.create-torrent-advanced', 'Comment'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-advanced')) .then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-advanced'))
// Click OK to create the torrent // Click OK to create the torrent
.then(() => app.client.click('.control.create-torrent')) .then(() => app.client.click('.control.create-torrent-button'))
.then(() => app.client.waitUntilTextExists('.torrent-list', 'tmp.jpg')) .then(() => app.client.waitUntilTextExists('.torrent-list', 'tmp.jpg'))
.then(() => app.client.moveToObject('.torrent')) .then(() => app.client.moveToObject('.torrent'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-100-percent')) .then(() => setup.screenshotCreateOrCompare(app, t, 'create-torrent-100-percent'))

View File

@@ -1,13 +1,14 @@
const rimraf = require('rimraf')
const test = require('tape') const test = require('tape')
const fs = require('fs-extra')
const setup = require('./setup')
const config = require('./config') const config = require('./config')
const setup = require('./setup')
test('torrent-list: show download path missing', function (t) { test('torrent-list: show download path missing', function (t) {
setup.resetTestDataDir() setup.resetTestDataDir()
fs.removeSync(config.TEST_DIR_DOWNLOAD) rimraf.sync(config.TEST_DIR_DOWNLOAD)
t.timeoutAfter(10e3) t.timeoutAfter(20e3)
const app = setup.createApp() const app = setup.createApp()
setup.waitForLoad(app, t) setup.waitForLoad(app, t)
.then(() => app.client.getTitle()) .then(() => app.client.getTitle())
@@ -34,11 +35,11 @@ test('torrent-list: start, stop, and delete torrents', function (t) {
.then(() => app.client.moveToObject('.torrent')) .then(() => app.client.moveToObject('.torrent'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover'))
// Click download on the first torrent, start downloading // Click download on the first torrent, start downloading
.then(() => app.client.click('.icon.download')) .then(() => app.client.click('.download input'))
.then(() => app.client.waitUntilTextExists('.torrent-list', '276 MB')) .then(() => app.client.waitUntilTextExists('.torrent-list', '276 MB'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-start-download')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-start-download'))
// Click download on the first torrent again, stop downloading // Click download on the first torrent again, stop downloading
.then(() => app.client.click('.icon.download')) .then(() => app.client.click('.download input'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover-download')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-hover-download'))
// Click delete on the first torrent // Click delete on the first torrent
.then(() => app.client.click('.icon.delete')) .then(() => app.client.click('.icon.delete'))
@@ -72,7 +73,7 @@ test('torrent-list: expand torrent, unselect file', function (t) {
.then(() => app.client.click('#torrent-cosmos .icon.deselect-file')) .then(() => app.client.click('#torrent-cosmos .icon.deselect-file'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-deselect')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-deselect'))
// Start the torrent // Start the torrent
.then(() => app.client.click('#torrent-cosmos .icon.download')) .then(() => app.client.click('#torrent-cosmos .download input'))
.then(() => app.client.waitUntilTextExists('.torrent-list', '0%')) .then(() => app.client.waitUntilTextExists('.torrent-list', '0%'))
.then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-start')) .then(() => setup.screenshotCreateOrCompare(app, t, 'torrent-list-cosmos-expand-start'))
// Make sure that it creates all files EXCEPT the deslected one // Make sure that it creates all files EXCEPT the deslected one