Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf3b416662 | ||
|
|
736832c4e3 | ||
|
|
f08c0995a2 | ||
|
|
de8a4d1160 | ||
|
|
739c1f705e | ||
|
|
a32889291f | ||
|
|
1e7e4cafd4 | ||
|
|
d5bed6c50a | ||
|
|
8f827c9aae | ||
|
|
24fe033e2f | ||
|
|
85d04f931b | ||
|
|
41b111c8a8 | ||
|
|
72db60bb12 | ||
|
|
076eb009b9 | ||
|
|
789bd0ce82 | ||
|
|
5155fca0e4 | ||
|
|
3b6819f894 | ||
|
|
0dd1683298 | ||
|
|
2788d7433b | ||
|
|
504a2419f6 | ||
|
|
75e5316ba1 | ||
|
|
3be018521a | ||
|
|
6d375d5b5b | ||
|
|
5de39bd7e5 | ||
|
|
a08d576851 | ||
|
|
b8bdf65514 | ||
|
|
1c0c3d07ff | ||
|
|
832980eb9a | ||
|
|
7c158e9f2c | ||
|
|
a98d22ed72 | ||
|
|
63b7d34e29 | ||
|
|
67ae6061aa | ||
|
|
a8a861260e | ||
|
|
fc879d5801 | ||
|
|
2d2645e642 | ||
|
|
8e66f641ce | ||
|
|
82c49b5fc5 | ||
|
|
b57ee73035 | ||
|
|
ed8f493b8b | ||
|
|
9e853027da | ||
|
|
1e05487acd | ||
|
|
167da9dfd5 | ||
|
|
46e138a376 | ||
|
|
853db922f1 | ||
|
|
7bf51b11ee | ||
|
|
3a286ae978 | ||
|
|
82245f0b5c | ||
|
|
802a898394 | ||
|
|
2200fffa1e | ||
|
|
f4b2e78e72 | ||
|
|
ad1162c7de | ||
|
|
ed4daeb560 | ||
|
|
927ae16e4f | ||
|
|
917c89542b | ||
|
|
39570bd4d7 | ||
|
|
f368dfad81 | ||
|
|
205e17cc83 | ||
|
|
4e052c8059 | ||
|
|
0afecd6063 | ||
|
|
41511c5615 | ||
|
|
b9d39e3c64 | ||
|
|
c1f482a950 | ||
|
|
e424031ad9 | ||
|
|
f43dc2fc98 | ||
|
|
9cdc73edce | ||
|
|
3d254fa075 | ||
|
|
ed1e43015e | ||
|
|
e6cbbd73f0 | ||
|
|
67dff7b38c | ||
|
|
ced67176a3 | ||
|
|
00ac8afe64 | ||
|
|
a6964c4495 | ||
|
|
aedbc3c32f | ||
|
|
6541291e0d | ||
|
|
711d274398 | ||
|
|
6c5861b9fc | ||
|
|
f7ab27f9fd | ||
|
|
e4e789cc5b | ||
|
|
09b525fe58 | ||
|
|
9dabfc1367 | ||
|
|
bdb733352a | ||
|
|
75a4655a0f | ||
|
|
051c1516a0 | ||
|
|
62c5b78358 | ||
|
|
290913d07a | ||
|
|
77534d650a | ||
|
|
a4c715e3f6 | ||
|
|
7415d3cee5 | ||
|
|
e1ba9c89fe | ||
|
|
0fcbe7369a | ||
|
|
c8087b5b63 | ||
|
|
065faca8eb | ||
|
|
bcd6a38a05 | ||
|
|
fa67f9b82b | ||
|
|
39acd0bd47 | ||
|
|
c549fcfc27 | ||
|
|
45e838d4c3 | ||
|
|
64f49e4d4f | ||
|
|
61caa90901 | ||
|
|
3e85289318 | ||
|
|
a629f287f0 | ||
|
|
3a4906079b | ||
|
|
3edf21f457 | ||
|
|
785c44cd2a | ||
|
|
1ad8a5313b | ||
|
|
967e161288 | ||
|
|
fe8c3b190c | ||
|
|
993e7d77ad | ||
|
|
e0be052df4 | ||
|
|
d331bae548 | ||
|
|
d88229694a | ||
|
|
8da5b955d6 | ||
|
|
54882679c1 | ||
|
|
f2007be1b0 | ||
|
|
7a757f9e05 |
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
npm-debug.log.*
|
npm-debug.log*
|
||||||
|
|||||||
@@ -32,5 +32,7 @@
|
|||||||
- Vamsi Krishna Avula (vamsi_ism@outlook.com)
|
- Vamsi Krishna Avula (vamsi_ism@outlook.com)
|
||||||
- Noam Okman (noamokman@gmail.com)
|
- Noam Okman (noamokman@gmail.com)
|
||||||
- PurgingPanda (t3ch0wn3r@gmail.com)
|
- PurgingPanda (t3ch0wn3r@gmail.com)
|
||||||
|
- Kai Curtis (morecode@kcurtis.com)
|
||||||
|
- Omri Litov (omrilitov@gmail.com)
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
43
CHANGELOG.md
@@ -1,5 +1,48 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
|
## v0.17.0 - 2016-09-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Remember window size and position
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Torrent list redesign
|
||||||
|
- Quieter, more subtle sounds
|
||||||
|
- Got rid of the play button spinner, now goes to the player immediately
|
||||||
|
- Faster startup
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix bug where playback rate could go negative
|
||||||
|
- Don't hide header when moused over player controls
|
||||||
|
- Fix Delete Data File on Windows
|
||||||
|
- Fix a sad, sad bug that resulted in 100+ MB config files
|
||||||
|
- Fix app DMG background image
|
||||||
|
|
||||||
|
## v0.16.0 - 2016-09-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Windows 64-bit support!** ([#931](https://github.com/feross/webtorrent-desktop/pull/931))
|
||||||
|
- Existing 32-bit users will update to 64-bit automatically in next release
|
||||||
|
- 64-bit reduces likelihood of out-of-memory errors by increasing the address space
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Mac: Fix background image on .DMG
|
||||||
|
|
||||||
|
## v0.15.0 - 2016-09-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Option to start automatically on login
|
||||||
|
- Add integration tests
|
||||||
|
- Add more detailed telemetry to diagnose "buffer allocation failed"
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Disable playback controls while in external player (#909)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix several uncaught errors (#889, #891, #892)
|
||||||
|
- Update to the latest webtorrent.js, fixing some more uncaught errors
|
||||||
|
- Clicking on the "torrent finished" notification works again (#912)
|
||||||
|
|
||||||
## v0.14.0 - 2016-09-03
|
## v0.14.0 - 2016-09-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
26
README.md
@@ -56,6 +56,32 @@ Restart the app automatically every time code changes. Useful during development
|
|||||||
$ npm run watch
|
$ npm run watch
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Run linters
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run integration tests
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run integration-test
|
||||||
|
```
|
||||||
|
|
||||||
|
The integration tests use Spectron and Tape. They click through the app, taking screenshots and comparing each one to a reference. Why screenshots?
|
||||||
|
|
||||||
|
* Ad-hoc checking makes the tests a lot more work to write
|
||||||
|
* Even diffing the whole HTML is not as thorough as screenshot diffing. For example, it wouldn't catch an bug where hitting ESC from a video doesn't correctly restore window size.
|
||||||
|
* Chrome's own integration tests use screenshot diffing iirc
|
||||||
|
* Small UI changes will break a few tests, but the fix is as easy as deleting the offending screenshots and running the tests, which will recreate them with the new look.
|
||||||
|
* The resulting Github PR will then show, pixel by pixel, the exact UI changes that were made! Ses https://github.com/blog/817-behold-image-view-modes
|
||||||
|
|
||||||
|
For MacOS, you'll need a Retina screen for the integration tests to pass. Your screen should have the same resolution as a 2016 12" Macbook.
|
||||||
|
|
||||||
|
For Windows, you'll need Windows 10 with a 1366x768 screen.
|
||||||
|
|
||||||
|
When running integration tests, keep the mouse on the edge of the screen and don't touch the mouse or keyboard while the tests are running.
|
||||||
|
|
||||||
### Package the app
|
### Package the app
|
||||||
|
|
||||||
Builds app binaries for Mac, Linux, and Windows.
|
Builds app binaries for Mac, Linux, and Windows.
|
||||||
|
|||||||
125
bin/package.js
@@ -39,9 +39,9 @@ function build () {
|
|||||||
rimraf.sync(DIST_PATH)
|
rimraf.sync(DIST_PATH)
|
||||||
rimraf.sync(BUILD_PATH)
|
rimraf.sync(BUILD_PATH)
|
||||||
|
|
||||||
console.log('Babel: Building JSX...')
|
console.log('Build: Transpiling to ES5...')
|
||||||
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
|
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
|
||||||
console.log('Babel: Built JSX.')
|
console.log('Build: Transpiled to ES5.')
|
||||||
|
|
||||||
var platform = argv._[0]
|
var platform = argv._[0]
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
@@ -73,11 +73,11 @@ var all = {
|
|||||||
// Package the application's source code into an archive, using Electron's archive
|
// Package the application's source code into an archive, using Electron's archive
|
||||||
// format. Mitigates issues around long path names on Windows and slightly speeds up
|
// format. Mitigates issues around long path names on Windows and slightly speeds up
|
||||||
// require().
|
// require().
|
||||||
asar: true,
|
asar: {
|
||||||
|
// A glob expression, that unpacks the files with matching names to the
|
||||||
// A glob expression, that unpacks the files with matching names to the
|
// "app.asar.unpacked" directory.
|
||||||
// "app.asar.unpacked" directory.
|
unpack: 'WebTorrent*'
|
||||||
'asar-unpack': 'WebTorrent*',
|
},
|
||||||
|
|
||||||
// The build version of the application. Maps to the FileVersion metadata property on
|
// The build version of the application. Maps to the FileVersion metadata property on
|
||||||
// Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to
|
// Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to
|
||||||
@@ -112,7 +112,7 @@ var darwin = {
|
|||||||
// Build for Mac
|
// Build for Mac
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
|
|
||||||
// Build 64 bit binaries only.
|
// Build x64 binaries only.
|
||||||
arch: 'x64',
|
arch: 'x64',
|
||||||
|
|
||||||
// The bundle identifier to use in the application's plist (Mac only).
|
// The bundle identifier to use in the application's plist (Mac only).
|
||||||
@@ -133,11 +133,11 @@ var win32 = {
|
|||||||
// Build for Windows.
|
// Build for Windows.
|
||||||
platform: 'win32',
|
platform: 'win32',
|
||||||
|
|
||||||
// Build 32 bit binaries only.
|
// Build ia32 and x64 binaries.
|
||||||
arch: 'ia32',
|
arch: 'all',
|
||||||
|
|
||||||
// Object hash of application metadata to embed into the executable (Windows only)
|
// Object hash of application metadata to embed into the executable (Windows only)
|
||||||
'version-string': {
|
win32metadata: {
|
||||||
|
|
||||||
// Company that produced the file.
|
// Company that produced the file.
|
||||||
CompanyName: config.APP_NAME,
|
CompanyName: config.APP_NAME,
|
||||||
@@ -167,7 +167,7 @@ var linux = {
|
|||||||
// Build for Linux.
|
// Build for Linux.
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
|
|
||||||
// Build 32 and 64 bit binaries.
|
// Build ia32 and x64 binaries.
|
||||||
arch: 'all'
|
arch: 'all'
|
||||||
|
|
||||||
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
||||||
@@ -388,19 +388,25 @@ function buildWin32 (cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tasks = []
|
var tasks = []
|
||||||
if (argv.package === 'exe' || argv.package === 'all') {
|
buildPath.forEach(function (filesPath) {
|
||||||
tasks.push((cb) => packageInstaller(cb))
|
var destArch = filesPath.split('-').pop()
|
||||||
}
|
|
||||||
if (argv.package === 'portable' || argv.package === 'all') {
|
if (argv.package === 'exe' || argv.package === 'all') {
|
||||||
tasks.push((cb) => packagePortable(cb))
|
tasks.push((cb) => packageInstaller(filesPath, destArch, cb))
|
||||||
}
|
}
|
||||||
|
if (argv.package === 'portable' || argv.package === 'all') {
|
||||||
|
tasks.push((cb) => packagePortable(filesPath, destArch, cb))
|
||||||
|
}
|
||||||
|
})
|
||||||
series(tasks, cb)
|
series(tasks, cb)
|
||||||
|
|
||||||
function packageInstaller (cb) {
|
function packageInstaller (filesPath, destArch, cb) {
|
||||||
console.log('Windows: Creating installer...')
|
console.log(`Windows: Creating ${destArch} installer...`)
|
||||||
|
|
||||||
|
var archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||||
|
|
||||||
installer.createWindowsInstaller({
|
installer.createWindowsInstaller({
|
||||||
appDirectory: buildPath[0],
|
appDirectory: filesPath,
|
||||||
authors: config.APP_TEAM,
|
authors: config.APP_TEAM,
|
||||||
description: config.APP_NAME,
|
description: config.APP_NAME,
|
||||||
exe: config.APP_NAME + '.exe',
|
exe: config.APP_NAME + '.exe',
|
||||||
@@ -410,8 +416,21 @@ function buildWin32 (cb) {
|
|||||||
noMsi: true,
|
noMsi: true,
|
||||||
outputDirectory: DIST_PATH,
|
outputDirectory: DIST_PATH,
|
||||||
productName: config.APP_NAME,
|
productName: config.APP_NAME,
|
||||||
remoteReleases: config.GITHUB_URL,
|
/**
|
||||||
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
* Only create delta updates for the Windows x64 build because 90% of our
|
||||||
|
* users have Windows x64 and the delta files take a *very* long time to
|
||||||
|
* generate. Also, the ia32 files on GitHub have non-standard Squirrel
|
||||||
|
* names (i.e. RELEASES-ia32 instead of RELEASES) and so Squirrel won't
|
||||||
|
* find them unless we proxy the requests.
|
||||||
|
*/
|
||||||
|
remoteReleases: destArch === 'x64'
|
||||||
|
? config.GITHUB_URL
|
||||||
|
: undefined,
|
||||||
|
/**
|
||||||
|
* If you hit a "GitHub API rate limit exceeded" error, set this token!
|
||||||
|
*/
|
||||||
|
remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
|
||||||
|
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe',
|
||||||
setupIcon: config.APP_ICON + '.ico',
|
setupIcon: config.APP_ICON + '.ico',
|
||||||
signWithParams: signWithParams,
|
signWithParams: signWithParams,
|
||||||
title: config.APP_NAME,
|
title: config.APP_NAME,
|
||||||
@@ -419,23 +438,65 @@ function buildWin32 (cb) {
|
|||||||
version: pkg.version
|
version: pkg.version
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
console.log('Windows: Created installer.')
|
console.log(`Windows: Created ${destArch} installer.`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete extraneous Squirrel files (i.e. *.nupkg delta files for older
|
||||||
|
* versions of the app)
|
||||||
|
*/
|
||||||
|
fs.readdirSync(DIST_PATH)
|
||||||
|
.filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version))
|
||||||
|
.forEach((filename) => {
|
||||||
|
fs.unlinkSync(path.join(DIST_PATH, filename))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (destArch === 'ia32') {
|
||||||
|
console.log('Windows: Renaming ia32 installer files...')
|
||||||
|
|
||||||
|
// RELEASES -> RELEASES-ia32
|
||||||
|
var relPath = path.join(DIST_PATH, 'RELEASES-ia32')
|
||||||
|
fs.renameSync(
|
||||||
|
path.join(DIST_PATH, 'RELEASES'),
|
||||||
|
relPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebTorrent-vX.X.X-full.nupkg -> WebTorrent-vX.X.X-ia32-full.nupkg
|
||||||
|
fs.renameSync(
|
||||||
|
path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-full.nupkg`),
|
||||||
|
path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-ia32-full.nupkg`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Change file name inside RELEASES-ia32 to match renamed file
|
||||||
|
var relContent = fs.readFileSync(relPath, 'utf8')
|
||||||
|
var relContent32 = relContent.replace('full.nupkg', 'ia32-full.nupkg')
|
||||||
|
fs.writeFileSync(relPath, relContent32)
|
||||||
|
|
||||||
|
if (relContent === relContent32) {
|
||||||
|
// Sanity check
|
||||||
|
throw new Error('Fixing RELEASES-ia32 failed. Replacement did not modify the file.')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Windows: Renamed ia32 installer files.')
|
||||||
|
}
|
||||||
|
|
||||||
cb(null)
|
cb(null)
|
||||||
})
|
})
|
||||||
.catch(cb)
|
.catch(cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
function packagePortable (cb) {
|
function packagePortable (filesPath, destArch, cb) {
|
||||||
console.log('Windows: Creating portable app...')
|
console.log(`Windows: Creating ${destArch} portable app...`)
|
||||||
|
|
||||||
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
var portablePath = path.join(filesPath, 'Portable Settings')
|
||||||
mkdirp.sync(portablePath)
|
mkdirp.sync(portablePath)
|
||||||
|
|
||||||
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
var archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
|
||||||
|
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||||
|
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip')
|
||||||
zip.zipSync(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log('Windows: Created portable app.')
|
console.log(`Windows: Created ${destArch} portable app.`)
|
||||||
cb(null)
|
cb(null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -500,8 +561,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' : ''
|
||||||
|
|
||||||
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
|
var 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.`)
|
||||||
|
|||||||
25
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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.14.0",
|
"version": "0.17.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@webtorrent.io",
|
"email": "feross@webtorrent.io",
|
||||||
@@ -16,13 +16,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"airplayer": "^2.0.0",
|
"airplayer": "^2.0.0",
|
||||||
"application-config": "^1.0.0",
|
"application-config": "^1.0.0",
|
||||||
|
"arch": "^2.0.0",
|
||||||
|
"auto-launch": "^4.0.1",
|
||||||
"bitfield": "^1.0.2",
|
"bitfield": "^1.0.2",
|
||||||
|
"capture-frame": "^1.0.0",
|
||||||
"chromecasts": "^1.8.0",
|
"chromecasts": "^1.8.0",
|
||||||
"create-torrent": "^3.24.5",
|
"create-torrent": "^3.24.5",
|
||||||
|
"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.3.3",
|
"electron": "1.4.1",
|
||||||
|
"es6-error": "^3.0.1",
|
||||||
"fs-extra": "^0.30.0",
|
"fs-extra": "^0.30.0",
|
||||||
"iso-639-1": "^1.2.1",
|
"iso-639-1": "^1.2.1",
|
||||||
"languagedetect": "^1.1.1",
|
"languagedetect": "^1.1.1",
|
||||||
@@ -46,15 +51,11 @@
|
|||||||
"zero-fill": "^2.2.3"
|
"zero-fill": "^2.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.11.4",
|
"buble": "^0.14.0",
|
||||||
"babel-plugin-syntax-jsx": "^6.13.0",
|
|
||||||
"babel-plugin-transform-es2015-destructuring": "^6.9.0",
|
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
|
||||||
"babel-plugin-transform-react-jsx": "^6.8.0",
|
|
||||||
"cross-zip": "^2.0.1",
|
"cross-zip": "^2.0.1",
|
||||||
"depcheck": "^0.6.4",
|
"depcheck": "^0.6.4",
|
||||||
"electron-osx-sign": "^0.3.0",
|
"electron-osx-sign": "^0.3.0",
|
||||||
"electron-packager": "^7.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",
|
||||||
@@ -63,9 +64,12 @@
|
|||||||
"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",
|
||||||
"rimraf": "^2.5.2",
|
"rimraf": "^2.5.2",
|
||||||
"run-series": "^1.1.4",
|
"run-series": "^1.1.4",
|
||||||
|
"spectron": "^3.3.0",
|
||||||
"standard": "*",
|
"standard": "*",
|
||||||
|
"tape": "^4.6.0",
|
||||||
"walk-sync": "^0.3.1"
|
"walk-sync": "^0.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -93,13 +97,14 @@
|
|||||||
"url": "git://github.com/feross/webtorrent-desktop.git"
|
"url": "git://github.com/feross/webtorrent-desktop.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "babel --quiet src --out-dir build",
|
"build": "buble src --output build",
|
||||||
"clean": "node ./bin/clean.js",
|
"clean": "node ./bin/clean.js",
|
||||||
"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 .",
|
||||||
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
|
"integration-test": "npm run build && node ./test",
|
||||||
|
"test": "standard && depcheck --ignores=buble,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
|
||||||
"gh-release": "gh-release",
|
"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,pug,css"
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": [
|
|
||||||
"syntax-jsx",
|
|
||||||
"transform-es2015-destructuring",
|
|
||||||
"transform-object-rest-spread",
|
|
||||||
"transform-react-jsx"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,26 @@
|
|||||||
const appConfig = require('application-config')('WebTorrent')
|
const appConfig = require('application-config')('WebTorrent')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const electron = require('electron')
|
||||||
|
const arch = require('arch')
|
||||||
|
|
||||||
const APP_NAME = 'WebTorrent'
|
const APP_NAME = 'WebTorrent'
|
||||||
const APP_TEAM = 'WebTorrent, LLC'
|
const APP_TEAM = 'WebTorrent, LLC'
|
||||||
const APP_VERSION = require('../package.json').version
|
const APP_VERSION = require('../package.json').version
|
||||||
|
|
||||||
const PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
const IS_TEST = isTest()
|
||||||
|
const PORTABLE_PATH = IS_TEST
|
||||||
|
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
|
||||||
|
: path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||||
|
const IS_PORTABLE = isPortable()
|
||||||
|
const IS_PRODUCTION = isProduction()
|
||||||
|
|
||||||
|
const UI_HEADER_HEIGHT = 38
|
||||||
|
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',
|
||||||
@@ -26,26 +40,31 @@ module.exports = {
|
|||||||
|
|
||||||
DEFAULT_TORRENTS: [
|
DEFAULT_TORRENTS: [
|
||||||
{
|
{
|
||||||
|
testID: 'bbb',
|
||||||
name: 'Big Buck Bunny',
|
name: 'Big Buck Bunny',
|
||||||
posterFileName: 'bigBuckBunny.jpg',
|
posterFileName: 'bigBuckBunny.jpg',
|
||||||
torrentFileName: 'bigBuckBunny.torrent'
|
torrentFileName: 'bigBuckBunny.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'cosmos',
|
||||||
name: 'Cosmos Laundromat (Preview)',
|
name: 'Cosmos Laundromat (Preview)',
|
||||||
posterFileName: 'cosmosLaundromat.jpg',
|
posterFileName: 'cosmosLaundromat.jpg',
|
||||||
torrentFileName: 'cosmosLaundromat.torrent'
|
torrentFileName: 'cosmosLaundromat.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'sintel',
|
||||||
name: 'Sintel',
|
name: 'Sintel',
|
||||||
posterFileName: 'sintel.jpg',
|
posterFileName: 'sintel.jpg',
|
||||||
torrentFileName: 'sintel.torrent'
|
torrentFileName: 'sintel.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'tears',
|
||||||
name: 'Tears of Steel',
|
name: 'Tears of Steel',
|
||||||
posterFileName: 'tearsOfSteel.jpg',
|
posterFileName: 'tearsOfSteel.jpg',
|
||||||
torrentFileName: 'tearsOfSteel.torrent'
|
torrentFileName: 'tearsOfSteel.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
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'
|
||||||
@@ -62,8 +81,11 @@ module.exports = {
|
|||||||
|
|
||||||
HOME_PAGE_URL: 'https://webtorrent.io',
|
HOME_PAGE_URL: 'https://webtorrent.io',
|
||||||
|
|
||||||
IS_PORTABLE: isPortable(),
|
IS_PORTABLE: IS_PORTABLE,
|
||||||
IS_PRODUCTION: isProduction(),
|
IS_PRODUCTION: IS_PRODUCTION,
|
||||||
|
IS_TEST: IS_TEST,
|
||||||
|
|
||||||
|
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
|
||||||
|
|
||||||
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
ROOT_PATH: path.join(__dirname, '..'),
|
ROOT_PATH: path.join(__dirname, '..'),
|
||||||
@@ -74,12 +96,19 @@ module.exports = {
|
|||||||
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
|
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
|
||||||
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
|
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
|
||||||
|
|
||||||
WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents
|
WINDOW_INITIAL_BOUNDS: {
|
||||||
WINDOW_MIN_WIDTH: 425
|
width: 500,
|
||||||
|
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
|
||||||
|
},
|
||||||
|
WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents
|
||||||
|
WINDOW_MIN_WIDTH: 425,
|
||||||
|
|
||||||
|
UI_HEADER_HEIGHT: UI_HEADER_HEIGHT,
|
||||||
|
UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfigPath () {
|
function getConfigPath () {
|
||||||
if (isPortable()) {
|
if (IS_PORTABLE) {
|
||||||
return PORTABLE_PATH
|
return PORTABLE_PATH
|
||||||
} else {
|
} else {
|
||||||
return path.dirname(appConfig.filePath)
|
return path.dirname(appConfig.filePath)
|
||||||
@@ -89,22 +118,31 @@ function getConfigPath () {
|
|||||||
function getDefaultDownloadPath () {
|
function getDefaultDownloadPath () {
|
||||||
if (!process || !process.type) {
|
if (!process || !process.type) {
|
||||||
return ''
|
return ''
|
||||||
}
|
} else if (IS_PORTABLE) {
|
||||||
|
|
||||||
if (isPortable()) {
|
|
||||||
return path.join(getConfigPath(), 'Downloads')
|
return path.join(getConfigPath(), 'Downloads')
|
||||||
|
} else {
|
||||||
|
return getPath('downloads')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const electron = require('electron')
|
function getPath (key) {
|
||||||
|
if (process.type === 'renderer') {
|
||||||
|
return electron.remote.app.getPath(key)
|
||||||
|
} else {
|
||||||
|
return electron.app.getPath(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return process.type === 'renderer'
|
function isTest () {
|
||||||
? electron.remote.app.getPath('downloads')
|
return process.env.NODE_ENV === 'test'
|
||||||
: electron.app.getPath('downloads')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPortable () {
|
function isPortable () {
|
||||||
|
if (IS_TEST) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
return process.platform === 'win32' && IS_PRODUCTION && !!fs.statSync(PORTABLE_PATH)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,7 @@ function openSeedFile () {
|
|||||||
title: 'Select a file for the torrent.',
|
title: 'Select a file for the torrent.',
|
||||||
properties: [ 'openFile' ]
|
properties: [ 'openFile' ]
|
||||||
}
|
}
|
||||||
setTitle(opts.title)
|
showOpenSeed(opts)
|
||||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
|
||||||
resetTitle()
|
|
||||||
if (!Array.isArray(selectedPaths)) return
|
|
||||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -46,12 +41,7 @@ function openSeedDirectory () {
|
|||||||
title: 'Select a folder for the torrent.',
|
title: 'Select a folder for the torrent.',
|
||||||
properties: [ 'openDirectory' ]
|
properties: [ 'openDirectory' ]
|
||||||
}
|
}
|
||||||
setTitle(opts.title)
|
showOpenSeed(opts)
|
||||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
|
||||||
resetTitle()
|
|
||||||
if (!Array.isArray(selectedPaths)) return
|
|
||||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -119,3 +109,16 @@ function setTitle (title) {
|
|||||||
function resetTitle () {
|
function resetTitle () {
|
||||||
windows.main.dispatch('resetTitle')
|
windows.main.dispatch('resetTitle')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pops up an Open File dialog with the given options.
|
||||||
|
* After the user selects files / folders, shows the Create Torrent page.
|
||||||
|
*/
|
||||||
|
function showOpenSeed (opts) {
|
||||||
|
setTitle(opts.title)
|
||||||
|
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||||
|
resetTitle()
|
||||||
|
if (!Array.isArray(selectedPaths)) return
|
||||||
|
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cp = require('child_process')
|
const cp = require('child_process')
|
||||||
|
const path = require('path')
|
||||||
const vlcCommand = require('vlc-command')
|
const vlcCommand = require('vlc-command')
|
||||||
|
|
||||||
const log = require('./log')
|
const log = require('./log')
|
||||||
@@ -13,15 +14,15 @@ const windows = require('./windows')
|
|||||||
// holds a ChildProcess while we're playing a video in an external player, null otherwise
|
// holds a ChildProcess while we're playing a video in an external player, null otherwise
|
||||||
let proc = null
|
let proc = null
|
||||||
|
|
||||||
function checkInstall (path, cb) {
|
function checkInstall (playerPath, cb) {
|
||||||
// check for VLC if external player has not been specified by the user
|
// check for VLC if external player has not been specified by the user
|
||||||
// otherwise assume the player is installed
|
// otherwise assume the player is installed
|
||||||
if (path == null) return vlcCommand((err) => cb(!err))
|
if (playerPath == null) return vlcCommand((err) => cb(!err))
|
||||||
process.nextTick(() => cb(true))
|
process.nextTick(() => cb(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawn (path, url, title) {
|
function spawn (playerPath, url, title) {
|
||||||
if (path != null) return spawnExternal(path, [url])
|
if (playerPath != null) return spawnExternal(playerPath, [url])
|
||||||
|
|
||||||
// Try to find and use VLC if external player is not specified
|
// Try to find and use VLC if external player is not specified
|
||||||
vlcCommand(function (err, vlcPath) {
|
vlcCommand(function (err, vlcPath) {
|
||||||
@@ -44,10 +45,15 @@ function kill () {
|
|||||||
proc = null
|
proc = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnExternal (path, args) {
|
function spawnExternal (playerPath, args) {
|
||||||
log('Running external media player:', path + ' ' + args.join(' '))
|
log('Running external media player:', playerPath + ' ' + args.join(' '))
|
||||||
|
|
||||||
proc = cp.spawn(path, args, {stdio: 'ignore'})
|
if (path.extname(playerPath) === '.app') {
|
||||||
|
// Mac: Use executable in packaged .app bundle
|
||||||
|
playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
|
||||||
|
}
|
||||||
|
|
||||||
|
proc = cp.spawn(playerPath, args, {stdio: 'ignore'})
|
||||||
|
|
||||||
// If it works, close the modal after a second
|
// If it works, close the modal after a second
|
||||||
const closeModalTimeout = setTimeout(() =>
|
const closeModalTimeout = setTimeout(() =>
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
const app = electron.app
|
const app = electron.app
|
||||||
const ipcMain = electron.ipcMain
|
|
||||||
|
|
||||||
const announcement = require('./announcement')
|
const parallel = require('run-parallel')
|
||||||
|
|
||||||
const config = require('../config')
|
const config = require('../config')
|
||||||
const crashReporter = require('../crash-reporter')
|
const crashReporter = require('../crash-reporter')
|
||||||
const dialog = require('./dialog')
|
|
||||||
const dock = require('./dock')
|
|
||||||
const ipc = require('./ipc')
|
const ipc = require('./ipc')
|
||||||
const log = require('./log')
|
const log = require('./log')
|
||||||
const menu = require('./menu')
|
const menu = require('./menu')
|
||||||
const squirrelWin32 = require('./squirrel-win32')
|
const State = require('../renderer/lib/state')
|
||||||
const tray = require('./tray')
|
|
||||||
const updater = require('./updater')
|
|
||||||
const userTasks = require('./user-tasks')
|
|
||||||
const windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
let shouldQuit = false
|
let shouldQuit = false
|
||||||
let argv = sliceArgv(process.argv)
|
let argv = sliceArgv(process.argv)
|
||||||
|
|
||||||
|
// Start the app without showing the main window when auto launching on login
|
||||||
|
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
|
||||||
|
const hidden = argv.includes('--hidden') ||
|
||||||
|
(process.platform === 'darwin' && app.getLoginItemSettings().wasOpenedAsHidden)
|
||||||
|
|
||||||
if (config.IS_PRODUCTION) {
|
if (config.IS_PRODUCTION) {
|
||||||
// When Electron is running in production mode (packaged app), then run React
|
// When Electron is running in production mode (packaged app), then run React
|
||||||
// in production mode too.
|
// in production mode too.
|
||||||
@@ -29,6 +28,7 @@ if (config.IS_PRODUCTION) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
|
const squirrelWin32 = require('./squirrel-win32')
|
||||||
shouldQuit = squirrelWin32.handleEvent(argv[0])
|
shouldQuit = squirrelWin32.handleEvent(argv[0])
|
||||||
argv = argv.filter((arg) => !arg.includes('--squirrel'))
|
argv = argv.filter((arg) => !arg.includes('--squirrel'))
|
||||||
}
|
}
|
||||||
@@ -51,24 +51,23 @@ function init () {
|
|||||||
app.setPath('userData', config.CONFIG_PATH)
|
app.setPath('userData', config.CONFIG_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ipcMain = electron.ipcMain
|
||||||
|
|
||||||
let isReady = false // app ready, windows can be created
|
let isReady = false // app ready, windows can be created
|
||||||
app.ipcReady = false // main window has finished loading and IPC is ready
|
app.ipcReady = false // main window has finished loading and IPC is ready
|
||||||
app.isQuitting = false
|
app.isQuitting = false
|
||||||
|
|
||||||
// Open handlers must be added as early as possible
|
parallel({
|
||||||
app.on('open-file', onOpen)
|
appReady: (cb) => app.on('ready', () => cb(null)),
|
||||||
app.on('open-url', onOpen)
|
state: (cb) => State.load(cb)
|
||||||
|
}, onReady)
|
||||||
|
|
||||||
ipc.init()
|
function onReady (err, results) {
|
||||||
|
if (err) throw err
|
||||||
|
|
||||||
app.once('will-finish-launching', function () {
|
|
||||||
crashReporter.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('ready', function () {
|
|
||||||
isReady = true
|
isReady = true
|
||||||
|
|
||||||
windows.main.init()
|
windows.main.init(results.state, {hidden: hidden})
|
||||||
windows.webtorrent.init()
|
windows.webtorrent.init()
|
||||||
menu.init()
|
menu.init()
|
||||||
|
|
||||||
@@ -81,6 +80,15 @@ function init () {
|
|||||||
const error = {message: err.message, stack: err.stack}
|
const error = {message: err.message, stack: err.stack}
|
||||||
windows.main.dispatch('uncaughtError', 'main', error)
|
windows.main.dispatch('uncaughtError', 'main', error)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on('open-file', onOpen)
|
||||||
|
app.on('open-url', onOpen)
|
||||||
|
|
||||||
|
ipc.init()
|
||||||
|
|
||||||
|
app.once('will-finish-launching', function () {
|
||||||
|
crashReporter.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.once('ipcReady', function () {
|
app.once('ipcReady', function () {
|
||||||
@@ -94,12 +102,12 @@ function init () {
|
|||||||
|
|
||||||
app.isQuitting = true
|
app.isQuitting = true
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
windows.main.dispatch('saveState') // try to save state on exit
|
windows.main.dispatch('stateSaveImmediate') // try to save state on exit
|
||||||
ipcMain.once('savedState', () => app.quit())
|
ipcMain.once('stateSaved', () => app.quit())
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.error('Saving state took too long. Quitting.')
|
console.error('Saving state took too long. Quitting.')
|
||||||
app.quit()
|
app.quit()
|
||||||
}, 2000) // quit after 2 secs, at most
|
}, 4000) // quit after 4 secs, at most
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
@@ -108,6 +116,12 @@ function init () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function delayedInit () {
|
function delayedInit () {
|
||||||
|
const announcement = require('./announcement')
|
||||||
|
const dock = require('./dock')
|
||||||
|
const tray = require('./tray')
|
||||||
|
const updater = require('./updater')
|
||||||
|
const userTasks = require('./user-tasks')
|
||||||
|
|
||||||
announcement.init()
|
announcement.init()
|
||||||
dock.init()
|
dock.init()
|
||||||
tray.init()
|
tray.init()
|
||||||
@@ -143,22 +157,38 @@ function onAppOpen (newArgv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove leading args.
|
||||||
|
// Production: 1 arg, eg: /Applications/WebTorrent.app/Contents/MacOS/WebTorrent
|
||||||
|
// Development: 2 args, eg: electron .
|
||||||
|
// Test: 4 args, eg: electron -r .../mocks.js .
|
||||||
function sliceArgv (argv) {
|
function sliceArgv (argv) {
|
||||||
return argv.slice(config.IS_PRODUCTION ? 1 : 2)
|
return argv.slice(config.IS_PRODUCTION ? 1
|
||||||
|
: config.IS_TEST ? 4
|
||||||
|
: 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
function processArgv (argv) {
|
function processArgv (argv) {
|
||||||
let torrentIds = []
|
let torrentIds = []
|
||||||
argv.forEach(function (arg) {
|
argv.forEach(function (arg) {
|
||||||
if (arg === '-n') {
|
if (arg === '-n' || arg === '-o' || arg === '-u') {
|
||||||
dialog.openSeedDirectory()
|
// Critical path: Only load the 'dialog' package if it is needed
|
||||||
} else if (arg === '-o') {
|
const dialog = require('./dialog')
|
||||||
dialog.openTorrentFile()
|
if (arg === '-n') {
|
||||||
} else if (arg === '-u') {
|
dialog.openSeedDirectory()
|
||||||
dialog.openTorrentAddress()
|
} else if (arg === '-o') {
|
||||||
|
dialog.openTorrentFile()
|
||||||
|
} else if (arg === '-u') {
|
||||||
|
dialog.openTorrentAddress()
|
||||||
|
}
|
||||||
|
} else if (arg === '--hidden') {
|
||||||
|
// Ignore hidden argument, already being handled
|
||||||
} else if (arg.startsWith('-psn')) {
|
} else if (arg.startsWith('-psn')) {
|
||||||
// Ignore Mac launchd "process serial number" argument
|
// Ignore Mac launchd "process serial number" argument
|
||||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
||||||
|
} else if (arg.startsWith('--')) {
|
||||||
|
// Ignore Spectron flags
|
||||||
|
} else if (arg === 'data:,') {
|
||||||
|
// Ignore weird Spectron argument
|
||||||
} else if (arg !== '.') {
|
} else if (arg !== '.') {
|
||||||
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
|
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
|
||||||
// development copy of WebTorrent is started while a production version is
|
// development copy of WebTorrent is started while a production version is
|
||||||
|
|||||||
@@ -6,17 +6,9 @@ const electron = require('electron')
|
|||||||
|
|
||||||
const app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
const dialog = require('./dialog')
|
|
||||||
const dock = require('./dock')
|
|
||||||
const handlers = require('./handlers')
|
|
||||||
const log = require('./log')
|
const log = require('./log')
|
||||||
const menu = require('./menu')
|
const menu = require('./menu')
|
||||||
const powerSaveBlocker = require('./power-save-blocker')
|
|
||||||
const shell = require('./shell')
|
|
||||||
const shortcuts = require('./shortcuts')
|
|
||||||
const externalPlayer = require('./external-player')
|
|
||||||
const windows = require('./windows')
|
const windows = require('./windows')
|
||||||
const thumbar = require('./thumbar')
|
|
||||||
|
|
||||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||||
const messageQueueMainToWebTorrent = []
|
const messageQueueMainToWebTorrent = []
|
||||||
@@ -43,45 +35,73 @@ function init () {
|
|||||||
* Dialog
|
* Dialog
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
|
ipc.on('openTorrentFile', () => {
|
||||||
ipc.on('openFiles', () => dialog.openFiles())
|
const dialog = require('./dialog')
|
||||||
|
dialog.openTorrentFile()
|
||||||
|
})
|
||||||
|
ipc.on('openFiles', () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openFiles()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dock
|
* Dock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
|
ipc.on('setBadge', (e, ...args) => {
|
||||||
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
|
const dock = require('./dock')
|
||||||
|
dock.setBadge(...args)
|
||||||
|
})
|
||||||
|
ipc.on('downloadFinished', (e, ...args) => {
|
||||||
|
const dock = require('./dock')
|
||||||
|
dock.downloadFinished(...args)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events
|
* Events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('onPlayerOpen', function () {
|
ipc.on('onPlayerOpen', function () {
|
||||||
menu.setPlayerOpen(true)
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
|
const shortcuts = require('./shortcuts')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
|
menu.togglePlaybackControls(true)
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
shortcuts.enable()
|
shortcuts.enable()
|
||||||
thumbar.enable()
|
thumbar.enable()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerUpdate', function (e, ...args) {
|
ipc.on('onPlayerUpdate', function (e, ...args) {
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
menu.onPlayerUpdate(...args)
|
menu.onPlayerUpdate(...args)
|
||||||
thumbar.onPlayerUpdate(...args)
|
thumbar.onPlayerUpdate(...args)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerClose', function () {
|
ipc.on('onPlayerClose', function () {
|
||||||
menu.setPlayerOpen(false)
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
|
const shortcuts = require('./shortcuts')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
|
menu.togglePlaybackControls(false)
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
shortcuts.disable()
|
shortcuts.disable()
|
||||||
thumbar.disable()
|
thumbar.disable()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerPlay', function () {
|
ipc.on('onPlayerPlay', function () {
|
||||||
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
thumbar.onPlayerPlay()
|
thumbar.onPlayerPlay()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerPause', function () {
|
ipc.on('onPlayerPause', function () {
|
||||||
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
thumbar.onPlayerPause()
|
thumbar.onPlayerPause()
|
||||||
})
|
})
|
||||||
@@ -90,18 +110,41 @@ function init () {
|
|||||||
* Shell
|
* Shell
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
ipc.on('openItem', (e, ...args) => {
|
||||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
const shell = require('./shell')
|
||||||
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
shell.openItem(...args)
|
||||||
|
})
|
||||||
|
ipc.on('showItemInFolder', (e, ...args) => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.showItemInFolder(...args)
|
||||||
|
})
|
||||||
|
ipc.on('moveItemToTrash', (e, ...args) => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.moveItemToTrash(...args)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File handlers
|
* File handlers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('setDefaultFileHandler', (e, flag) => {
|
ipc.on('setDefaultFileHandler', (e, flag) => {
|
||||||
|
const handlers = require('./handlers')
|
||||||
|
|
||||||
if (flag) handlers.install()
|
if (flag) handlers.install()
|
||||||
else handlers.uninstall()
|
else handlers.uninstall()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto start on login
|
||||||
|
*/
|
||||||
|
|
||||||
|
ipc.on('setStartup', (e, flag) => {
|
||||||
|
const startup = require('./startup')
|
||||||
|
|
||||||
|
if (flag) startup.install()
|
||||||
|
else startup.uninstall()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Windows: Main
|
* Windows: Main
|
||||||
*/
|
*/
|
||||||
@@ -121,15 +164,31 @@ function init () {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('checkForExternalPlayer', function (e, path) {
|
ipc.on('checkForExternalPlayer', function (e, path) {
|
||||||
|
const externalPlayer = require('./external-player')
|
||||||
|
|
||||||
externalPlayer.checkInstall(path, function (isInstalled) {
|
externalPlayer.checkInstall(path, function (isInstalled) {
|
||||||
windows.main.send('checkForExternalPlayer', isInstalled)
|
windows.main.send('checkForExternalPlayer', isInstalled)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
|
ipc.on('openExternalPlayer', (e, ...args) => {
|
||||||
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
const externalPlayer = require('./external-player')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
|
menu.togglePlaybackControls(false)
|
||||||
|
thumbar.disable()
|
||||||
|
externalPlayer.spawn(...args)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipc.on('quitExternalPlayer', () => {
|
||||||
|
const externalPlayer = require('./external-player')
|
||||||
|
externalPlayer.kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message passing
|
||||||
|
*/
|
||||||
|
|
||||||
// Capture all events
|
|
||||||
const oldEmit = ipc.emit
|
const oldEmit = ipc.emit
|
||||||
ipc.emit = function (name, e, ...args) {
|
ipc.emit = function (name, e, ...args) {
|
||||||
// Relay messages between the main window and the WebTorrent hidden window
|
// Relay messages between the main window and the WebTorrent hidden window
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
setPlayerOpen,
|
togglePlaybackControls,
|
||||||
setWindowFocus,
|
setWindowFocus,
|
||||||
setAllowNav,
|
setAllowNav,
|
||||||
onPlayerUpdate,
|
onPlayerUpdate,
|
||||||
@@ -13,8 +13,6 @@ const electron = require('electron')
|
|||||||
const app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
const config = require('../config')
|
const config = require('../config')
|
||||||
const dialog = require('./dialog')
|
|
||||||
const shell = require('./shell')
|
|
||||||
const windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
let menu = null
|
let menu = null
|
||||||
@@ -24,7 +22,7 @@ function init () {
|
|||||||
electron.Menu.setApplicationMenu(menu)
|
electron.Menu.setApplicationMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPlayerOpen (flag) {
|
function togglePlaybackControls (flag) {
|
||||||
getMenuItem('Play/Pause').enabled = flag
|
getMenuItem('Play/Pause').enabled = flag
|
||||||
getMenuItem('Skip Next').enabled = flag
|
getMenuItem('Skip Next').enabled = flag
|
||||||
getMenuItem('Skip Previous').enabled = flag
|
getMenuItem('Skip Previous').enabled = flag
|
||||||
@@ -90,17 +88,26 @@ function getMenuTemplate () {
|
|||||||
? 'Create New Torrent...'
|
? 'Create New Torrent...'
|
||||||
: 'Create New Torrent from Folder...',
|
: 'Create New Torrent from Folder...',
|
||||||
accelerator: 'CmdOrCtrl+N',
|
accelerator: 'CmdOrCtrl+N',
|
||||||
click: () => dialog.openSeedDirectory()
|
click: () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openSeedDirectory()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent File...',
|
label: 'Open Torrent File...',
|
||||||
accelerator: 'CmdOrCtrl+O',
|
accelerator: 'CmdOrCtrl+O',
|
||||||
click: () => dialog.openTorrentFile()
|
click: () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openTorrentFile()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent Address...',
|
label: 'Open Torrent Address...',
|
||||||
accelerator: 'CmdOrCtrl+U',
|
accelerator: 'CmdOrCtrl+U',
|
||||||
click: () => dialog.openTorrentAddress()
|
click: () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openTorrentAddress()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
@@ -277,18 +284,27 @@ function getMenuTemplate () {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Learn more about ' + config.APP_NAME,
|
label: 'Learn more about ' + config.APP_NAME,
|
||||||
click: () => shell.openExternal(config.HOME_PAGE_URL)
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.openExternal(config.HOME_PAGE_URL)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Contribute on GitHub',
|
label: 'Contribute on GitHub',
|
||||||
click: () => shell.openExternal(config.GITHUB_URL)
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.openExternal(config.GITHUB_URL)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Report an Issue...',
|
label: 'Report an Issue...',
|
||||||
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -361,7 +377,10 @@ function getMenuTemplate () {
|
|||||||
// File menu (Windows, Linux)
|
// File menu (Windows, Linux)
|
||||||
template[0].submenu.unshift({
|
template[0].submenu.unshift({
|
||||||
label: 'Create New Torrent from File...',
|
label: 'Create New Torrent from File...',
|
||||||
click: () => dialog.openSeedFile()
|
click: () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openSeedFile()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Edit menu (Windows, Linux)
|
// Edit menu (Windows, Linux)
|
||||||
|
|||||||
36
src/main/startup.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
module.exports = {
|
||||||
|
install,
|
||||||
|
uninstall
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = require('../config')
|
||||||
|
const AutoLaunch = require('auto-launch')
|
||||||
|
const { app } = require('electron')
|
||||||
|
|
||||||
|
// On Mac, work around a bug in auto-launch where it opens a Terminal window
|
||||||
|
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
|
||||||
|
const appPath = process.platform === 'darwin'
|
||||||
|
? app.getPath('exe').replace(/\.app\/Content.*/, '.app')
|
||||||
|
: undefined // Use the default
|
||||||
|
|
||||||
|
const appLauncher = new AutoLaunch({
|
||||||
|
name: config.APP_NAME,
|
||||||
|
path: appPath,
|
||||||
|
isHidden: true
|
||||||
|
})
|
||||||
|
|
||||||
|
function install () {
|
||||||
|
return appLauncher
|
||||||
|
.isEnabled()
|
||||||
|
.then(enabled => {
|
||||||
|
if (!enabled) return appLauncher.enable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninstall () {
|
||||||
|
return appLauncher
|
||||||
|
.isEnabled()
|
||||||
|
.then(enabled => {
|
||||||
|
if (enabled) return appLauncher.disable()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@ const windows = require('./windows')
|
|||||||
|
|
||||||
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||||
'?version=' + config.APP_VERSION +
|
'?version=' + config.APP_VERSION +
|
||||||
'&platform=' + process.platform
|
'&platform=' + process.platform +
|
||||||
|
'&sysarch=' + config.OS_SYSARCH
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function init () {
|
|||||||
// No menu on the About window
|
// No menu on the About window
|
||||||
win.setMenu(null)
|
win.setMenu(null)
|
||||||
|
|
||||||
win.webContents.once('did-finish-load', function () {
|
win.once('ready-to-show', function () {
|
||||||
win.show()
|
win.show()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -21,15 +21,14 @@ const app = electron.app
|
|||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
const log = require('../log')
|
const log = require('../log')
|
||||||
const menu = require('../menu')
|
const menu = require('../menu')
|
||||||
const tray = require('../tray')
|
|
||||||
|
|
||||||
const HEADER_HEIGHT = 38
|
function init (state, options) {
|
||||||
const TORRENT_HEIGHT = 100
|
|
||||||
|
|
||||||
function init () {
|
|
||||||
if (main.win) {
|
if (main.win) {
|
||||||
return main.win.show()
|
return main.win.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
|
||||||
|
|
||||||
const win = main.win = new electron.BrowserWindow({
|
const win = main.win = new electron.BrowserWindow({
|
||||||
backgroundColor: '#282828',
|
backgroundColor: '#282828',
|
||||||
darkTheme: true, // Forces dark theme (GTK+3)
|
darkTheme: true, // Forces dark theme (GTK+3)
|
||||||
@@ -39,13 +38,20 @@ function init () {
|
|||||||
title: config.APP_WINDOW_TITLE,
|
title: config.APP_WINDOW_TITLE,
|
||||||
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
|
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
|
||||||
useContentSize: true, // Specify web page size without OS chrome
|
useContentSize: true, // Specify web page size without OS chrome
|
||||||
width: 500,
|
show: false,
|
||||||
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
width: initialBounds.width,
|
||||||
|
height: initialBounds.height,
|
||||||
|
x: initialBounds.x,
|
||||||
|
y: initialBounds.y
|
||||||
|
})
|
||||||
|
|
||||||
|
win.once('ready-to-show', function () {
|
||||||
|
if (!options.hidden) win.show()
|
||||||
})
|
})
|
||||||
|
|
||||||
win.loadURL(config.WINDOW_MAIN)
|
win.loadURL(config.WINDOW_MAIN)
|
||||||
|
|
||||||
if (win.setSheetOffset) win.setSheetOffset(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())
|
||||||
@@ -69,7 +75,17 @@ function init () {
|
|||||||
win.setMenuBarVisibility(true)
|
win.setMenuBarVisibility(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
win.on('move', function (e) {
|
||||||
|
send('windowBoundsChanged', e.sender.getBounds())
|
||||||
|
})
|
||||||
|
|
||||||
|
win.on('resize', function (e) {
|
||||||
|
send('windowBoundsChanged', e.sender.getBounds())
|
||||||
|
})
|
||||||
|
|
||||||
win.on('close', function (e) {
|
win.on('close', function (e) {
|
||||||
|
const tray = require('../tray')
|
||||||
|
|
||||||
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
||||||
app.quit()
|
app.quit()
|
||||||
} else if (!app.isQuitting) {
|
} else if (!app.isQuitting) {
|
||||||
@@ -210,11 +226,15 @@ function toggleFullScreen (flag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
function onWindowBlur () {
|
||||||
|
const tray = require('../tray')
|
||||||
|
|
||||||
menu.setWindowFocus(false)
|
menu.setWindowFocus(false)
|
||||||
tray.setWindowFocus(false)
|
tray.setWindowFocus(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowFocus () {
|
function onWindowFocus () {
|
||||||
|
const tray = require('../tray')
|
||||||
|
|
||||||
menu.setWindowFocus(true)
|
menu.setWindowFocus(true)
|
||||||
tray.setWindowFocus(true)
|
tray.setWindowFocus(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ class Header extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
const loc = this.props.state.location
|
const loc = this.props.state.location
|
||||||
return (
|
return (
|
||||||
<div className='header'>
|
<div className='header'
|
||||||
|
onMouseMove={dispatcher('mediaMouseMoved')}
|
||||||
|
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
|
||||||
|
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
|
||||||
{this.getTitle()}
|
{this.getTitle()}
|
||||||
<div className='nav left float-left'>
|
<div className='nav left float-left'>
|
||||||
<i
|
<i
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ module.exports = class ModalOKCancel extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className='float-right'>
|
<div className='float-right'>
|
||||||
<FlatButton
|
<FlatButton
|
||||||
className='control'
|
className='control cancel'
|
||||||
style={cancelStyle}
|
style={cancelStyle}
|
||||||
label={cancelText}
|
label={cancelText}
|
||||||
onClick={onCancel} />
|
onClick={onCancel} />
|
||||||
<RaisedButton
|
<RaisedButton
|
||||||
className='control'
|
className='control ok'
|
||||||
primary
|
primary
|
||||||
label={okText}
|
label={okText}
|
||||||
onClick={onOK} />
|
onClick={onOK} />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ module.exports = class OpenTorrentAddressModal extends React.Component {
|
|||||||
<p><label>Enter torrent address or magnet link</label></p>
|
<p><label>Enter torrent address or magnet link</label></p>
|
||||||
<div>
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
|
id='torrent-address-field'
|
||||||
className='control'
|
className='control'
|
||||||
ref={(c) => { this.torrentURL = c }}
|
ref={(c) => { this.torrentURL = c }}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -31,7 +32,7 @@ module.exports = class OpenTorrentAddressModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown (e) {
|
function handleKeyDown (e) {
|
||||||
if (e.which === 13) this.handleOK() /* hit Enter to submit */
|
if (e.which === 13) handleOK.call(this) /* hit Enter to submit */
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOK () {
|
function handleOK () {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const React = require('react')
|
const React = require('react')
|
||||||
|
|
||||||
const FlatButton = require('material-ui/FlatButton').default
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
|
||||||
class ShowMore extends React.Component {
|
class ShowMore extends React.Component {
|
||||||
static get propTypes () {
|
static get propTypes () {
|
||||||
@@ -39,9 +39,10 @@ class ShowMore extends React.Component {
|
|||||||
? this.props.hideLabel
|
? this.props.hideLabel
|
||||||
: this.props.showLabel
|
: this.props.showLabel
|
||||||
return (
|
return (
|
||||||
<div style={this.props.style}>
|
<div className='show-more' style={this.props.style}>
|
||||||
{this.state.expanded ? this.props.children : null}
|
{this.state.expanded ? this.props.children : null}
|
||||||
<FlatButton
|
<RaisedButton
|
||||||
|
className='control'
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
label={label} />
|
label={label} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const React = require('react')
|
const React = require('react')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const ModalOKCancel = require('./modal-ok-cancel')
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
const {dispatcher} = require('../lib/dispatcher')
|
const {dispatcher} = require('../lib/dispatcher')
|
||||||
@@ -12,15 +11,11 @@ module.exports = class UnsupportedMediaModal extends React.Component {
|
|||||||
const message = (err && err.getMessage)
|
const message = (err && err.getMessage)
|
||||||
? err.getMessage()
|
? err.getMessage()
|
||||||
: err
|
: err
|
||||||
const playerPath = state.saved.prefs.externalPlayerPath
|
|
||||||
const playerName = playerPath
|
|
||||||
? path.basename(playerPath).split('.')[0]
|
|
||||||
: 'VLC'
|
|
||||||
const onAction = state.modal.externalPlayerInstalled
|
const onAction = state.modal.externalPlayerInstalled
|
||||||
? dispatcher('openExternalPlayer')
|
? dispatcher('openExternalPlayer')
|
||||||
: () => this.onInstall()
|
: () => this.onInstall()
|
||||||
const actionText = state.modal.externalPlayerInstalled
|
const actionText = state.modal.externalPlayerInstalled
|
||||||
? 'PLAY IN ' + playerName.toUpperCase()
|
? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase()
|
||||||
: 'INSTALL VLC'
|
: 'INSTALL VLC'
|
||||||
const errorMessage = state.modal.externalPlayerNotFound
|
const errorMessage = state.modal.externalPlayerNotFound
|
||||||
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ module.exports = class MediaController {
|
|||||||
this.state.playing.mouseStationarySince = new Date().getTime()
|
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
controlsMouseEnter () {
|
||||||
|
this.state.playing.mouseInControls = true
|
||||||
|
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
controlsMouseLeave () {
|
||||||
|
this.state.playing.mouseInControls = false
|
||||||
|
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
openExternalPlayer () {
|
openExternalPlayer () {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
const mediaURL = Playlist.getCurrentLocalURL(this.state)
|
const mediaURL = Playlist.getCurrentLocalURL(this.state)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const path = require('path')
|
|||||||
const Cast = require('../lib/cast')
|
const Cast = require('../lib/cast')
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
const telemetry = require('../lib/telemetry')
|
const telemetry = require('../lib/telemetry')
|
||||||
const errors = require('../lib/errors')
|
const {UnplayableFileError, UnplayableTorrentError} = require('../lib/errors')
|
||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
@@ -42,7 +42,7 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
|
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
|
||||||
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
|
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
|
||||||
if (index === undefined) return cb(new errors.UnplayableError())
|
if (index === undefined) return cb(new UnplayableTorrentError())
|
||||||
|
|
||||||
initialized = true
|
initialized = true
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ module.exports = class PlaybackController {
|
|||||||
// Play next file in list (if any)
|
// Play next file in list (if any)
|
||||||
nextTrack () {
|
nextTrack () {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
if (Playlist.hasNext(state)) {
|
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
|
||||||
this.updatePlayer(
|
this.updatePlayer(
|
||||||
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
||||||
if (err) dispatch('error', err)
|
if (err) dispatch('error', err)
|
||||||
@@ -99,7 +99,7 @@ module.exports = class PlaybackController {
|
|||||||
// Play previous track in list (if any)
|
// Play previous track in list (if any)
|
||||||
previousTrack () {
|
previousTrack () {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
if (Playlist.hasPrevious(state)) {
|
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
|
||||||
this.updatePlayer(
|
this.updatePlayer(
|
||||||
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
||||||
if (err) dispatch('error', err)
|
if (err) dispatch('error', err)
|
||||||
@@ -152,26 +152,21 @@ module.exports = class PlaybackController {
|
|||||||
changePlaybackRate (direction) {
|
changePlaybackRate (direction) {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
let rate = state.playing.playbackRate
|
let rate = state.playing.playbackRate
|
||||||
if (direction > 0 && rate >= 0.25 && rate < 2) {
|
if (direction > 0 && rate < 2) {
|
||||||
rate += 0.25
|
rate += 0.25
|
||||||
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
||||||
rate -= 0.25
|
rate -= 0.25
|
||||||
} else if (direction < 0 && rate === 0.25) {
|
} else if (direction > 0 && rate >= 1 && rate < 16) {
|
||||||
// When we set playback rate at 0 in html 5, playback hangs ;(
|
|
||||||
rate = -1
|
|
||||||
} else if (direction > 0 && rate === -1) {
|
|
||||||
rate = 0.25
|
|
||||||
} else if ((direction > 0 && rate >= 1 && rate < 16) ||
|
|
||||||
(direction < 0 && rate > -16 && rate <= -1)) {
|
|
||||||
rate *= 2
|
rate *= 2
|
||||||
} else if ((direction < 0 && rate > 1 && rate <= 16) ||
|
} else if (direction < 0 && rate > 1 && rate <= 16) {
|
||||||
(direction > 0 && rate >= -16 && rate < -1)) {
|
|
||||||
rate /= 2
|
rate /= 2
|
||||||
}
|
}
|
||||||
state.playing.playbackRate = rate
|
state.playing.playbackRate = rate
|
||||||
if (isCasting(state) && !Cast.setRate(rate)) {
|
if (isCasting(state) && !Cast.setRate(rate)) {
|
||||||
state.playing.playbackRate = 1
|
state.playing.playbackRate = 1
|
||||||
}
|
}
|
||||||
|
// Wait a bit before we hide the controls and header again
|
||||||
|
state.playing.mouseStationarySince = new Date().getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the volume, in range [0, 1], by some amount
|
// Change the volume, in range [0, 1], by some amount
|
||||||
@@ -201,11 +196,7 @@ module.exports = class PlaybackController {
|
|||||||
// * The video is playing remotely on Chromecast or Airplay
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
showOrHidePlayerControls () {
|
showOrHidePlayerControls () {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
const hideControls = state.location.url() === 'player' &&
|
const hideControls = state.shouldHidePlayerControls()
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
|
||||||
!state.playing.isPaused &&
|
|
||||||
state.playing.location === 'local'
|
|
||||||
|
|
||||||
if (hideControls !== state.playing.hideControls) {
|
if (hideControls !== state.playing.hideControls) {
|
||||||
state.playing.hideControls = hideControls
|
state.playing.hideControls = hideControls
|
||||||
@@ -222,38 +213,15 @@ module.exports = class PlaybackController {
|
|||||||
state.playing.infoHash = torrentSummary.infoHash
|
state.playing.infoHash = torrentSummary.infoHash
|
||||||
|
|
||||||
// update UI to show pending playback
|
// update UI to show pending playback
|
||||||
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
sound.play('PLAY')
|
||||||
// TODO: remove torrentSummary.playStatus
|
|
||||||
torrentSummary.playStatus = 'requested'
|
|
||||||
this.update()
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
this.startServer(torrentSummary)
|
||||||
telemetry.logPlayAttempt('timeout')
|
ipcRenderer.send('onPlayerOpen')
|
||||||
// TODO: remove torrentSummary.playStatus
|
this.updatePlayer(infoHash, index, true, cb)
|
||||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
|
||||||
sound.play('ERROR')
|
|
||||||
cb(new Error('Playback timed out. Try again.'))
|
|
||||||
this.update()
|
|
||||||
}, 10000) /* give it a few seconds */
|
|
||||||
|
|
||||||
this.startServer(torrentSummary, () => {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
|
|
||||||
// if we timed out (user clicked play a long time ago), don't autoplay
|
|
||||||
const timedOut = torrentSummary.playStatus === 'timeout'
|
|
||||||
delete torrentSummary.playStatus
|
|
||||||
if (timedOut) {
|
|
||||||
ipcRenderer.send('wt-stop-server')
|
|
||||||
return this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerOpen')
|
|
||||||
this.updatePlayer(infoHash, index, true, cb)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts WebTorrent server for media streaming
|
// Starts WebTorrent server for media streaming
|
||||||
startServer (torrentSummary, cb) {
|
startServer (torrentSummary) {
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
||||||
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||||
@@ -264,7 +232,6 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
function onTorrentReady () {
|
function onTorrentReady () {
|
||||||
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
|
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
|
||||||
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, () => cb())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +244,7 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
||||||
torrentSummary.mostRecentFileIndex = undefined
|
torrentSummary.mostRecentFileIndex = undefined
|
||||||
return cb(new Error('Can\'t play that file'))
|
return cb(new UnplayableFileError())
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentSummary.mostRecentFileIndex = index
|
torrentSummary.mostRecentFileIndex = index
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
const State = require('../lib/state')
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
const ipcRenderer = require('electron').ipcRenderer
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
@@ -17,7 +16,9 @@ module.exports = class PrefsController {
|
|||||||
setup: function (cb) {
|
setup: function (cb) {
|
||||||
// initialize preferences
|
// initialize preferences
|
||||||
state.window.title = 'Preferences'
|
state.window.title = 'Preferences'
|
||||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
state.unsaved = Object.assign(state.unsaved || {}, {
|
||||||
|
prefs: Object.assign({}, state.saved.prefs)
|
||||||
|
})
|
||||||
ipcRenderer.send('setAllowNav', false)
|
ipcRenderer.send('setAllowNav', false)
|
||||||
cb()
|
cb()
|
||||||
},
|
},
|
||||||
@@ -50,8 +51,11 @@ module.exports = class PrefsController {
|
|||||||
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
|
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
|
||||||
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
|
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
|
||||||
}
|
}
|
||||||
|
if (state.unsaved.prefs.startup !== state.saved.prefs.startup) {
|
||||||
|
ipcRenderer.send('setStartup', state.unsaved.prefs.startup)
|
||||||
|
}
|
||||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||||
State.save(state)
|
dispatch('stateSaveImmediate')
|
||||||
dispatch('checkDownloadPath')
|
dispatch('checkDownloadPath')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,10 @@ module.exports = class SubtitlesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSubtitles (files, autoSelect) {
|
addSubtitles (files, autoSelect) {
|
||||||
const state = this.state
|
|
||||||
// Subtitles are only supported when playing video files
|
// Subtitles are only supported when playing video files
|
||||||
if (state.playing.type !== 'video') return
|
if (this.state.playing.type !== 'video') return
|
||||||
if (files.length === 0) return
|
if (files.length === 0) return
|
||||||
const subtitles = state.playing.subtitles
|
const subtitles = this.state.playing.subtitles
|
||||||
|
|
||||||
// Read the files concurrently, then add all resulting subtitle tracks
|
// Read the files concurrently, then add all resulting subtitle tracks
|
||||||
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||||
@@ -47,17 +46,17 @@ module.exports = class SubtitlesController {
|
|||||||
for (let i = 0; i < tracks.length; i++) {
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
// No dupes allowed
|
// No dupes allowed
|
||||||
const track = tracks[i]
|
const track = tracks[i]
|
||||||
let trackIndex = state.playing.subtitles.tracks
|
let trackIndex = subtitles.tracks.findIndex((t) =>
|
||||||
.findIndex((t) => track.filePath === t.filePath)
|
track.filePath === t.filePath)
|
||||||
|
|
||||||
// Add the track
|
// Add the track
|
||||||
if (trackIndex === -1) {
|
if (trackIndex === -1) {
|
||||||
trackIndex = state.playing.subtitles.tracks.push(track) - 1
|
trackIndex = subtitles.tracks.push(track) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're auto-selecting a track, try to find one in the user's language
|
// If we're auto-selecting a track, try to find one in the user's language
|
||||||
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||||
state.playing.subtitles.selectedIndex = trackIndex
|
subtitles.selectedIndex = trackIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,21 +127,22 @@ module.exports = class TorrentController {
|
|||||||
|
|
||||||
torrentFileModtimes (torrentKey, fileModtimes) {
|
torrentFileModtimes (torrentKey, fileModtimes) {
|
||||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
if (!torrentSummary) throw new Error('Not saving modtimes for deleted torrent ' + torrentKey)
|
||||||
torrentSummary.fileModtimes = fileModtimes
|
torrentSummary.fileModtimes = fileModtimes
|
||||||
dispatch('saveStateThrottled')
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentFileSaved (torrentKey, torrentFileName) {
|
torrentFileSaved (torrentKey, torrentFileName) {
|
||||||
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
||||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.torrentFileName = torrentFileName
|
torrentSummary.torrentFileName = torrentFileName
|
||||||
dispatch('saveStateThrottled')
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentPosterSaved (torrentKey, posterFileName) {
|
torrentPosterSaved (torrentKey, posterFileName) {
|
||||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.posterFileName = posterFileName
|
torrentSummary.posterFileName = posterFileName
|
||||||
dispatch('saveStateThrottled')
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentAudioMetadata (infoHash, index, info) {
|
torrentAudioMetadata (infoHash, index, info) {
|
||||||
@@ -176,7 +177,7 @@ function showDoneNotification (torrent) {
|
|||||||
silent: true
|
silent: true
|
||||||
})
|
})
|
||||||
|
|
||||||
notif.onClick = function () {
|
notif.onclick = function () {
|
||||||
ipcRenderer.send('show')
|
ipcRenderer.send('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const path = require('path')
|
|||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
const State = require('../lib/state')
|
const {TorrentKeyNotFoundError} = require('../lib/errors')
|
||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ module.exports = class TorrentListController {
|
|||||||
// Starts downloading and/or seeding a given torrentSummary.
|
// Starts downloading and/or seeding a given torrentSummary.
|
||||||
startTorrentingSummary (torrentKey) {
|
startTorrentingSummary (torrentKey) {
|
||||||
const s = TorrentSummary.getByKey(this.state, torrentKey)
|
const s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
if (!s) throw new Error('Missing key: ' + torrentKey)
|
if (!s) throw new TorrentKeyNotFoundError(torrentKey)
|
||||||
|
|
||||||
// New torrent: give it a path
|
// New torrent: give it a path
|
||||||
if (!s.path) {
|
if (!s.path) {
|
||||||
@@ -127,7 +127,9 @@ module.exports = class TorrentListController {
|
|||||||
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
||||||
|
|
||||||
// Let the WebTorrent process know to start or stop fetching that file
|
// Let the WebTorrent process know to start or stop fetching that file
|
||||||
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
if (torrentSummary.status !== 'paused') {
|
||||||
|
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmDeleteTorrent (infoHash, deleteData) {
|
confirmDeleteTorrent (infoHash, deleteData) {
|
||||||
@@ -156,7 +158,7 @@ module.exports = class TorrentListController {
|
|||||||
|
|
||||||
// remove torrent from saved list
|
// remove torrent from saved list
|
||||||
this.state.saved.torrents.splice(index, 1)
|
this.state.saved.torrents.splice(index, 1)
|
||||||
State.saveThrottled(this.state)
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent user from going forward to a deleted torrent
|
// prevent user from going forward to a deleted torrent
|
||||||
@@ -212,11 +214,41 @@ module.exports = class TorrentListController {
|
|||||||
|
|
||||||
menu.append(new electron.remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Save Torrent File As...',
|
label: 'Save Torrent File As...',
|
||||||
click: () => saveTorrentFileAs(torrentSummary)
|
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.popup(electron.remote.getCurrentWindow())
|
menu.popup(electron.remote.getCurrentWindow())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Takes a torrentSummary or torrentKey
|
||||||
|
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
|
||||||
|
saveTorrentFileAs (torrentKey) {
|
||||||
|
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
|
if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
|
||||||
|
const downloadPath = this.state.saved.prefs.downloadPath
|
||||||
|
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||||
|
const win = electron.remote.getCurrentWindow()
|
||||||
|
const opts = {
|
||||||
|
title: 'Save Torrent File',
|
||||||
|
defaultPath: path.join(downloadPath, newFileName),
|
||||||
|
filters: [
|
||||||
|
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||||
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
|
||||||
|
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
|
||||||
|
if (!savePath) return // They clicked Cancel
|
||||||
|
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
|
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
fs.writeFile(savePath, torrentFile, function (err) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively finds {name, path, size} for all files in a folder
|
// Recursively finds {name, path, size} for all files in a folder
|
||||||
@@ -277,27 +309,3 @@ function moveItemToTrash (torrentSummary) {
|
|||||||
function showItemInFolder (torrentSummary) {
|
function showItemInFolder (torrentSummary) {
|
||||||
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTorrentFileAs (torrentSummary) {
|
|
||||||
const downloadPath = this.state.saved.prefs.downloadPath
|
|
||||||
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
|
||||||
const opts = {
|
|
||||||
title: 'Save Torrent File',
|
|
||||||
defaultPath: path.join(downloadPath, newFileName),
|
|
||||||
filters: [
|
|
||||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
|
||||||
{ name: 'All Files', extensions: ['*'] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
const win = electron.remote.getCurrentWindow()
|
|
||||||
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
|
|
||||||
if (!savePath) return // They clicked Cancel
|
|
||||||
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
|
||||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
|
||||||
if (err) return dispatch('error', err)
|
|
||||||
fs.writeFile(savePath, torrentFile, function (err) {
|
|
||||||
if (err) return dispatch('error', err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const State = require('../lib/state')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
// Controls the UI checking for new versions of the app, prompting install
|
// Controls the UI checking for new versions of the app, prompting install
|
||||||
module.exports = class UpdateController {
|
module.exports = class UpdateController {
|
||||||
@@ -21,6 +21,6 @@ module.exports = class UpdateController {
|
|||||||
let skipped = this.state.saved.skippedVersions
|
let skipped = this.state.saved.skippedVersions
|
||||||
if (!skipped) skipped = this.state.saved.skippedVersions = []
|
if (!skipped) skipped = this.state.saved.skippedVersions = []
|
||||||
skipped.push(version)
|
skipped.push(version)
|
||||||
State.saveThrottled(this.state)
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
module.exports = captureVideoFrame
|
|
||||||
|
|
||||||
function captureVideoFrame (video, format) {
|
|
||||||
if (typeof video === 'string') {
|
|
||||||
video = document.querySelector(video)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (video == null || video.nodeName !== 'VIDEO') {
|
|
||||||
throw new Error('First argument must be a <video> element or selector')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format == null) {
|
|
||||||
format = 'png'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
|
|
||||||
throw new Error('Second argument must be one of "png", "jpg", or "webp"')
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = video.videoWidth
|
|
||||||
canvas.height = video.videoHeight
|
|
||||||
|
|
||||||
canvas.getContext('2d').drawImage(video, 0, 0)
|
|
||||||
|
|
||||||
const dataUri = canvas.toDataURL('image/' + format)
|
|
||||||
const data = dataUri.split(',')[1]
|
|
||||||
|
|
||||||
return new Buffer(data, 'base64')
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
|
const {CastingError} = require('./errors')
|
||||||
|
|
||||||
// Lazy load these for a ~300ms improvement in startup time
|
// Lazy load these for a ~300ms improvement in startup time
|
||||||
let airplayer, chromecasts, dlnacasts
|
let airplayer, chromecasts, dlnacasts
|
||||||
@@ -32,6 +33,15 @@ function init (appState, callback) {
|
|||||||
state = appState
|
state = appState
|
||||||
update = callback
|
update = callback
|
||||||
|
|
||||||
|
// Don't actually cast during integration tests
|
||||||
|
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
|
||||||
|
if (config.IS_TEST) {
|
||||||
|
state.devices.chromecast = testPlayer('chromecast')
|
||||||
|
state.devices.airplay = testPlayer('airplay')
|
||||||
|
state.devices.dlna = testPlayer('dlna')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Load modules, scan the network for devices
|
// Load modules, scan the network for devices
|
||||||
airplayer = require('airplayer')()
|
airplayer = require('airplayer')()
|
||||||
chromecasts = require('chromecasts')()
|
chromecasts = require('chromecasts')()
|
||||||
@@ -57,6 +67,32 @@ function init (appState, callback) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// integration test player implementation
|
||||||
|
function testPlayer (type) {
|
||||||
|
return {
|
||||||
|
getDevices,
|
||||||
|
open,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
stop,
|
||||||
|
status,
|
||||||
|
seek,
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDevices () {
|
||||||
|
return [{name: type + '-1'}, {name: type + '-2'}]
|
||||||
|
}
|
||||||
|
|
||||||
|
function open () {}
|
||||||
|
function play () {}
|
||||||
|
function pause () {}
|
||||||
|
function stop () {}
|
||||||
|
function status () {}
|
||||||
|
function seek () {}
|
||||||
|
function volume () {}
|
||||||
|
}
|
||||||
|
|
||||||
// chromecast player implementation
|
// chromecast player implementation
|
||||||
function chromecastPlayer () {
|
function chromecastPlayer () {
|
||||||
const ret = {
|
const ret = {
|
||||||
@@ -126,13 +162,7 @@ function chromecastPlayer () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
ret.device.status(function (err, status) {
|
ret.device.status(handleStatus)
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
|
||||||
state.playing.currentTime = status.currentTime
|
|
||||||
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
@@ -306,13 +336,7 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
ret.device.status(function (err, status) {
|
ret.device.status(handleStatus)
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
|
||||||
state.playing.currentTime = status.currentTime
|
|
||||||
state.playing.volume = status.volume.level
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
@@ -328,6 +352,18 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStatus (err, status) {
|
||||||
|
if (err || !status) {
|
||||||
|
return console.log('error getting %s status: %o',
|
||||||
|
state.playing.location,
|
||||||
|
err || 'missing response')
|
||||||
|
}
|
||||||
|
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||||
|
state.playing.currentTime = status.currentTime
|
||||||
|
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
// Start polling cast device state, whenever we're connected
|
// Start polling cast device state, whenever we're connected
|
||||||
function startStatusInterval () {
|
function startStatusInterval () {
|
||||||
statusInterval = setInterval(function () {
|
statusInterval = setInterval(function () {
|
||||||
@@ -350,14 +386,16 @@ function toggleMenu (location) {
|
|||||||
|
|
||||||
// Never cast to two devices at the same time
|
// Never cast to two devices at the same time
|
||||||
if (state.playing.location !== 'local') {
|
if (state.playing.location !== 'local') {
|
||||||
throw new Error('You can\'t connect to ' + location +
|
throw new CastingError(
|
||||||
' when already connected to another device')
|
`You can't connect to ${location} when already connected to another device`
|
||||||
}
|
) }
|
||||||
|
|
||||||
// Find all cast devices of the given type
|
// Find all cast devices of the given type
|
||||||
const player = getPlayer(location)
|
const player = getPlayer(location)
|
||||||
const devices = player ? player.getDevices() : []
|
const devices = player ? player.getDevices() : []
|
||||||
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
if (devices.length === 0) {
|
||||||
|
throw new CastingError(`No ${location} devices available`)
|
||||||
|
}
|
||||||
|
|
||||||
// Show a menu
|
// Show a menu
|
||||||
state.devices.castMenu = {location, devices}
|
state.devices.castMenu = {location, devices}
|
||||||
|
|||||||
@@ -1,15 +1,44 @@
|
|||||||
|
const ExtendableError = require('es6-error')
|
||||||
|
|
||||||
|
/* Generic errors */
|
||||||
|
|
||||||
|
class CastingError extends ExtendableError {}
|
||||||
|
class PlaybackError extends ExtendableError {}
|
||||||
|
class SoundError extends ExtendableError {}
|
||||||
|
class TorrentError extends ExtendableError {}
|
||||||
|
|
||||||
|
/* Playback */
|
||||||
|
|
||||||
|
class UnplayableTorrentError extends PlaybackError {
|
||||||
|
constructor () { super('Can\'t play any files in torrent') }
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnplayableFileError extends PlaybackError {
|
||||||
|
constructor () { super('Can\'t play that file') }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sound */
|
||||||
|
|
||||||
|
class InvalidSoundNameError extends SoundError {
|
||||||
|
constructor (name) { super(`Invalid sound name: ${name}`) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Torrent */
|
||||||
|
|
||||||
|
class TorrentKeyNotFoundError extends TorrentError {
|
||||||
|
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidTorrentError extends TorrentError {}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
CastingError,
|
||||||
|
PlaybackError,
|
||||||
|
SoundError,
|
||||||
|
TorrentError,
|
||||||
UnplayableTorrentError,
|
UnplayableTorrentError,
|
||||||
UnplayableFileError
|
UnplayableFileError,
|
||||||
|
InvalidSoundNameError,
|
||||||
|
TorrentKeyNotFoundError,
|
||||||
|
InvalidTorrentError
|
||||||
}
|
}
|
||||||
|
|
||||||
function UnplayableTorrentError () {
|
|
||||||
this.message = 'Can\'t play any files in torrent'
|
|
||||||
}
|
|
||||||
|
|
||||||
function UnplayableFileError () {
|
|
||||||
this.message = 'Can\'t play that file'
|
|
||||||
}
|
|
||||||
|
|
||||||
UnplayableTorrentError.prototype = Error
|
|
||||||
UnplayableFileError.prototype = Error
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ module.exports = {
|
|||||||
run
|
run
|
||||||
}
|
}
|
||||||
|
|
||||||
const semver = require('semver')
|
|
||||||
const config = require('../../config')
|
|
||||||
const TorrentSummary = require('./torrent-summary')
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const semver = require('semver')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
// Change `state.saved` (which will be saved back to config.json on exit) as
|
// Change `state.saved` (which will be saved back to config.json on exit) as
|
||||||
// needed, for example to deal with config.json format changes across versions
|
// needed, for example to deal with config.json format changes across versions
|
||||||
@@ -39,6 +39,10 @@ function run (state) {
|
|||||||
migrate_0_14_0(state.saved)
|
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
|
||||||
}
|
}
|
||||||
@@ -112,6 +116,8 @@ function migrate_0_11_0 (saved) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_12_0 (saved) {
|
function migrate_0_12_0 (saved) {
|
||||||
|
const TorrentSummary = require('./torrent-summary')
|
||||||
|
|
||||||
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
|
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
|
||||||
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
|
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
|
||||||
}
|
}
|
||||||
@@ -145,3 +151,15 @@ function migrate_0_14_0 (saved) {
|
|||||||
delete ts.defaultPlayFileIndex
|
delete ts.defaultPlayFileIndex
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrate_0_17_0 (saved) {
|
||||||
|
// Fix a sad, sad bug that resulted in 100MB+ config.json files
|
||||||
|
saved.torrents.forEach(function (ts) {
|
||||||
|
if (!ts.files) return
|
||||||
|
ts.files.forEach(function (file) {
|
||||||
|
if (!file.audioInfo || !file.audioInfo.picture) return
|
||||||
|
// This contained a Buffer, which 30x'd in size when serialized to JSON
|
||||||
|
delete file.audioInfo.picture
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ function getPreviousIndex (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentLocalURL (state) {
|
function getCurrentLocalURL (state) {
|
||||||
return state.server.localURL + '/' + state.playing.fileIndex
|
return state.server
|
||||||
|
? state.server.localURL + '/' + state.playing.fileIndex
|
||||||
|
: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCache (state) {
|
function updateCache (state) {
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
|
const {InvalidSoundNameError} = require('./errors')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const VOLUME = 0.15
|
const VOLUME = 0.3
|
||||||
|
|
||||||
/* Cache of Audio elements, for instant playback */
|
/* Cache of Audio elements, for instant playback */
|
||||||
const cache = {}
|
const cache = {}
|
||||||
@@ -18,7 +19,7 @@ const sounds = {
|
|||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
||||||
volume: VOLUME
|
volume: VOLUME * 0.5
|
||||||
},
|
},
|
||||||
DISABLE: {
|
DISABLE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
||||||
@@ -38,11 +39,11 @@ 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
|
volume: VOLUME * 1.25
|
||||||
},
|
},
|
||||||
STARTUP: {
|
STARTUP: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
||||||
volume: VOLUME * 2
|
volume: VOLUME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ function play (name) {
|
|||||||
if (!audio) {
|
if (!audio) {
|
||||||
const sound = sounds[name]
|
const sound = sounds[name]
|
||||||
if (!sound) {
|
if (!sound) {
|
||||||
throw new Error('Invalid sound name')
|
throw new InvalidSoundNameError(name)
|
||||||
}
|
}
|
||||||
audio = cache[name] = new window.Audio()
|
audio = cache[name] = new window.Audio()
|
||||||
audio.volume = sound.volume
|
audio.volume = sound.volume
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
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 migrations = require('./migrations')
|
||||||
|
|
||||||
|
const SAVE_DEBOUNCE_INTERVAL = 1000
|
||||||
|
|
||||||
const State = module.exports = Object.assign(new EventEmitter(), {
|
const State = module.exports = Object.assign(new EventEmitter(), {
|
||||||
getDefaultPlayState,
|
getDefaultPlayState,
|
||||||
load,
|
load,
|
||||||
save,
|
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
|
||||||
saveThrottled
|
save: debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL),
|
||||||
|
saveImmediate
|
||||||
})
|
})
|
||||||
|
|
||||||
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||||
@@ -68,7 +72,9 @@ function getDefaultState () {
|
|||||||
* Getters, for convenience
|
* Getters, for convenience
|
||||||
*/
|
*/
|
||||||
getPlayingTorrentSummary,
|
getPlayingTorrentSummary,
|
||||||
getPlayingFileSummary
|
getPlayingFileSummary,
|
||||||
|
getExternalPlayerName,
|
||||||
|
shouldHidePlayerControls
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +103,7 @@ 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 setupSavedState (cb) {
|
function setupStateSaved (cb) {
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const parseTorrent = require('parse-torrent')
|
const parseTorrent = require('parse-torrent')
|
||||||
const parallel = require('run-parallel')
|
const parallel = require('run-parallel')
|
||||||
@@ -107,7 +113,8 @@ function setupSavedState (cb) {
|
|||||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
||||||
isFileHandler: false,
|
isFileHandler: false,
|
||||||
openExternalPlayer: false,
|
openExternalPlayer: false,
|
||||||
externalPlayerPath: null
|
externalPlayerPath: null,
|
||||||
|
startup: false
|
||||||
},
|
},
|
||||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||||
@@ -151,7 +158,8 @@ function setupSavedState (cb) {
|
|||||||
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
||||||
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
||||||
files: parsedTorrent.files,
|
files: parsedTorrent.files,
|
||||||
selections: parsedTorrent.files.map((x) => true)
|
selections: parsedTorrent.files.map((x) => true),
|
||||||
|
testID: t.testID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,20 +175,35 @@ function getPlayingFileSummary () {
|
|||||||
return torrentSummary.files[this.playing.fileIndex]
|
return torrentSummary.files[this.playing.fileIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
function load (cb) {
|
function getExternalPlayerName () {
|
||||||
const state = getDefaultState()
|
const playerPath = this.saved.prefs.externalPlayerPath
|
||||||
|
if (!playerPath) return 'VLC'
|
||||||
|
return path.basename(playerPath).split('.')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldHidePlayerControls () {
|
||||||
|
return this.location.url() === 'player' &&
|
||||||
|
this.playing.mouseStationarySince !== 0 &&
|
||||||
|
new Date().getTime() - this.playing.mouseStationarySince > 2000 &&
|
||||||
|
!this.playing.mouseInControls &&
|
||||||
|
!this.playing.isPaused &&
|
||||||
|
this.playing.location === 'local' &&
|
||||||
|
this.playing.playbackRate === 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function load (cb) {
|
||||||
appConfig.read(function (err, saved) {
|
appConfig.read(function (err, saved) {
|
||||||
if (err || !saved.version) {
|
if (err || !saved.version) {
|
||||||
console.log('Missing config file: Creating new one')
|
console.log('Missing config file: Creating new one')
|
||||||
setupSavedState(onSaved)
|
setupStateSaved(onSavedState)
|
||||||
} else {
|
} else {
|
||||||
onSaved(null, saved)
|
onSavedState(null, saved)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function onSaved (err, saved) {
|
function onSavedState (err, saved) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
|
const state = getDefaultState()
|
||||||
state.saved = saved
|
state.saved = saved
|
||||||
migrations.run(state)
|
migrations.run(state)
|
||||||
cb(null, state)
|
cb(null, state)
|
||||||
@@ -188,9 +211,8 @@ function load (cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write state.saved to the JSON state file
|
// Write state.saved to the JSON state file
|
||||||
function save (state, cb) {
|
function saveImmediate (state, cb) {
|
||||||
console.log('Saving state to ' + appConfig.filePath)
|
console.log('Saving state to ' + appConfig.filePath)
|
||||||
delete state.saveStateTimeout
|
|
||||||
|
|
||||||
// Clean up, so that we're not saving any pending state
|
// Clean up, so that we're not saving any pending state
|
||||||
const copy = Object.assign({}, state.saved)
|
const copy = Object.assign({}, state.saved)
|
||||||
@@ -204,9 +226,6 @@ function save (state, cb) {
|
|||||||
if (key === 'progress' || key === 'torrentKey') {
|
if (key === 'progress' || key === 'torrentKey') {
|
||||||
continue // Don't save progress info or key for the webtorrent process
|
continue // Don't save progress info or key for the webtorrent process
|
||||||
}
|
}
|
||||||
if (key === 'playStatus') {
|
|
||||||
continue // Don't save whether a torrent is playing / pending
|
|
||||||
}
|
|
||||||
if (key === 'error') {
|
if (key === 'error') {
|
||||||
continue // Don't save error states
|
continue // Don't save error states
|
||||||
}
|
}
|
||||||
@@ -217,15 +236,6 @@ function save (state, cb) {
|
|||||||
|
|
||||||
appConfig.write(copy, (err) => {
|
appConfig.write(copy, (err) => {
|
||||||
if (err) console.error(err)
|
if (err) console.error(err)
|
||||||
else State.emit('savedState')
|
else State.emit('stateSaved')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write, but no more than once a second
|
|
||||||
function saveThrottled (state) {
|
|
||||||
if (state.saveStateTimeout) return
|
|
||||||
state.saveStateTimeout = setTimeout(function () {
|
|
||||||
if (!state.saveStateTimeout) return
|
|
||||||
save(state)
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ function init (state) {
|
|||||||
telemetry.localTime = now.toTimeString()
|
telemetry.localTime = now.toTimeString()
|
||||||
telemetry.screens = getScreenInfo()
|
telemetry.screens = getScreenInfo()
|
||||||
telemetry.system = getSystemInfo()
|
telemetry.system = getSystemInfo()
|
||||||
telemetry.approxNumTorrents = getApproxNumTorrents(state)
|
telemetry.torrentStats = getTorrentStats(state)
|
||||||
|
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
|
||||||
|
|
||||||
if (config.IS_PRODUCTION) {
|
if (config.IS_PRODUCTION) {
|
||||||
postToServer()
|
postToServer()
|
||||||
@@ -104,18 +105,64 @@ function getSystemInfo () {
|
|||||||
osPlatform: process.platform,
|
osPlatform: process.platform,
|
||||||
osRelease: os.type() + ' ' + os.release(),
|
osRelease: os.type() + ' ' + os.release(),
|
||||||
architecture: os.arch(),
|
architecture: os.arch(),
|
||||||
totalMemoryMB: os.totalmem() / (1 << 20),
|
systemArchitecture: config.OS_SYSARCH,
|
||||||
|
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
|
||||||
numCores: os.cpus().length
|
numCores: os.cpus().length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the number of torrents, rounded to the nearest power of two
|
// Get stats like the # of torrents currently active, # in list, total size
|
||||||
function getApproxNumTorrents (state) {
|
function getTorrentStats (state) {
|
||||||
const exactNum = state.saved.torrents.length
|
const count = state.saved.torrents.length
|
||||||
if (exactNum === 0) return 0
|
let sizeMB = 0
|
||||||
|
let byStatus = {
|
||||||
|
new: { count: 0, sizeMB: 0 },
|
||||||
|
downloading: { count: 0, sizeMB: 0 },
|
||||||
|
seeding: { count: 0, sizeMB: 0 },
|
||||||
|
paused: { count: 0, sizeMB: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, count torrents & total file size
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const t = state.saved.torrents[i]
|
||||||
|
const stat = byStatus[t.status]
|
||||||
|
if (!t || !t.files || !stat) continue
|
||||||
|
stat.count++
|
||||||
|
for (let j = 0; j < t.files.length; j++) {
|
||||||
|
const f = t.files[j]
|
||||||
|
if (!f || !f.length) continue
|
||||||
|
const fileSizeMB = f.length / (1 << 20)
|
||||||
|
sizeMB += fileSizeMB
|
||||||
|
stat.sizeMB += fileSizeMB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, round all the counts and sums to the nearest power of 2
|
||||||
|
const ret = roundTorrentStats({count, sizeMB})
|
||||||
|
ret.byStatus = {
|
||||||
|
new: roundTorrentStats(byStatus.new),
|
||||||
|
downloading: roundTorrentStats(byStatus.downloading),
|
||||||
|
seeding: roundTorrentStats(byStatus.seeding),
|
||||||
|
paused: roundTorrentStats(byStatus.paused)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundTorrentStats (stats) {
|
||||||
|
return {
|
||||||
|
approxCount: roundPow2(stats.count),
|
||||||
|
approxSizeMB: roundPow2(stats.sizeMB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounds to the nearest power of 2, for privacy and easy bucketing.
|
||||||
|
// Rounds 35 to 32, 70 to 64, 5 to 4, 1 to 1, 0 to 0.
|
||||||
|
// Supports nonnegative numbers only.
|
||||||
|
function roundPow2 (n) {
|
||||||
|
if (n <= 0) return 0
|
||||||
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||||
const log2 = Math.log(exactNum) / Math.log(2)
|
const log2 = Math.log(n) / Math.log(2)
|
||||||
return 1 << Math.round(log2)
|
return Math.pow(2, Math.round(log2))
|
||||||
}
|
}
|
||||||
|
|
||||||
// An uncaught error happened in the main process or in one of the windows
|
// An uncaught error happened in the main process or in one of the windows
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = torrentPoster
|
module.exports = torrentPoster
|
||||||
|
|
||||||
const captureVideoFrame = require('./capture-video-frame')
|
const captureFrame = require('capture-frame')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
function torrentPoster (torrent, cb) {
|
function torrentPoster (torrent, cb) {
|
||||||
@@ -61,7 +61,7 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
|||||||
function onSeeked () {
|
function onSeeked () {
|
||||||
video.removeEventListener('seeked', onSeeked)
|
video.removeEventListener('seeked', onSeeked)
|
||||||
|
|
||||||
const buf = captureVideoFrame(video)
|
const buf = captureFrame(video)
|
||||||
|
|
||||||
// unload video element
|
// unload video element
|
||||||
video.pause()
|
video.pause()
|
||||||
|
|||||||
@@ -53,5 +53,6 @@ function getByKey (state, torrentKey) {
|
|||||||
function getFileOrFolder (torrentSummary) {
|
function getFileOrFolder (torrentSummary) {
|
||||||
const ts = torrentSummary
|
const ts = torrentSummary
|
||||||
if (!ts.path || !ts.files || ts.files.length === 0) return null
|
if (!ts.path || !ts.files || ts.files.length === 0) return null
|
||||||
return path.join(ts.path, ts.files[0].path.split('/')[0])
|
const dirname = ts.files[0].path.split(path.sep)[0]
|
||||||
|
return path.join(ts.path, dirname)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ State.load(onState)
|
|||||||
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
|
state = window.state = _state // Make available for easier debugging
|
||||||
|
window.dispatch = dispatch
|
||||||
|
|
||||||
telemetry.init(state)
|
telemetry.init(state)
|
||||||
|
|
||||||
@@ -122,7 +123,9 @@ function onState (err, _state) {
|
|||||||
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
||||||
|
|
||||||
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||||
sound.play('STARTUP')
|
if (electron.remote.getCurrentWindow().isVisible()) {
|
||||||
|
sound.play('STARTUP')
|
||||||
|
}
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +194,8 @@ const dispatchHandlers = {
|
|||||||
controllers.torrentList.openTorrentContextMenu(infoHash),
|
controllers.torrentList.openTorrentContextMenu(infoHash),
|
||||||
'startTorrentingSummary': (torrentKey) =>
|
'startTorrentingSummary': (torrentKey) =>
|
||||||
controllers.torrentList.startTorrentingSummary(torrentKey),
|
controllers.torrentList.startTorrentingSummary(torrentKey),
|
||||||
|
'saveTorrentFileAs': (torrentKey) =>
|
||||||
|
controllers.torrentList.saveTorrentFileAs(torrentKey),
|
||||||
|
|
||||||
// Playback
|
// Playback
|
||||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
||||||
@@ -217,6 +222,8 @@ const dispatchHandlers = {
|
|||||||
'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(),
|
||||||
|
'mediaControlsMouseLeave': () => controllers.media.controlsMouseLeave(),
|
||||||
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
|
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
|
||||||
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
|
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
|
||||||
|
|
||||||
@@ -252,8 +259,8 @@ const dispatchHandlers = {
|
|||||||
'onOpen': onOpen,
|
'onOpen': onOpen,
|
||||||
'error': onError,
|
'error': onError,
|
||||||
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
|
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
|
||||||
'saveState': () => State.save(state),
|
'stateSave': () => State.save(state),
|
||||||
'saveStateThrottled': () => State.saveThrottled(state),
|
'stateSaveImmediate': () => State.saveImmediate(state),
|
||||||
'update': () => {} // No-op, just trigger an update
|
'update': () => {} // No-op, just trigger an update
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +290,7 @@ function setupIpc () {
|
|||||||
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
|
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
|
||||||
|
|
||||||
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
|
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
|
||||||
|
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))
|
||||||
@@ -302,7 +310,7 @@ function setupIpc () {
|
|||||||
|
|
||||||
ipcRenderer.send('ipcReady')
|
ipcRenderer.send('ipcReady')
|
||||||
|
|
||||||
State.on('savedState', () => ipcRenderer.send('savedState'))
|
State.on('stateSaved', () => ipcRenderer.send('stateSaved'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quits any modal popovers and returns to the torrent list screen
|
// Quits any modal popovers and returns to the torrent list screen
|
||||||
@@ -457,6 +465,13 @@ function onFullscreenChanged (e, isFullScreen) {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onWindowBoundsChanged (e, newBounds) {
|
||||||
|
if (state.location.url() !== 'player') {
|
||||||
|
state.saved.bounds = newBounds
|
||||||
|
dispatch('stateSave')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function checkDownloadPath () {
|
function checkDownloadPath () {
|
||||||
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
|
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ const fontFamily = process.platform === 'win32'
|
|||||||
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
|
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
|
||||||
lightBaseTheme.fontFamily = fontFamily
|
lightBaseTheme.fontFamily = fontFamily
|
||||||
darkBaseTheme.fontFamily = fontFamily
|
darkBaseTheme.fontFamily = fontFamily
|
||||||
darkBaseTheme.palette.primary1Color = colors.cyan500
|
darkBaseTheme.palette.primary1Color = colors.grey50
|
||||||
darkBaseTheme.palette.primary2Color = colors.cyan500
|
darkBaseTheme.palette.primary2Color = colors.grey50
|
||||||
darkBaseTheme.palette.primary3Color = colors.grey600
|
darkBaseTheme.palette.primary3Color = colors.grey600
|
||||||
darkBaseTheme.palette.accent1Color = colors.redA200
|
darkBaseTheme.palette.accent1Color = colors.redA200
|
||||||
darkBaseTheme.palette.accent2Color = colors.redA400
|
darkBaseTheme.palette.accent2Color = colors.redA400
|
||||||
@@ -43,12 +43,7 @@ class App extends React.Component {
|
|||||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||||
// * The video is paused
|
// * The video is paused
|
||||||
// * The video is playing remotely on Chromecast or Airplay
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
const hideControls = state.location.url() === 'player' &&
|
const hideControls = state.shouldHidePlayerControls()
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
|
||||||
!state.playing.isPaused &&
|
|
||||||
state.playing.location === 'local' &&
|
|
||||||
state.playing.playbackRate === 1
|
|
||||||
|
|
||||||
const cls = [
|
const cls = [
|
||||||
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
||||||
|
|||||||
@@ -99,14 +99,14 @@ class CreateTorrentPage extends React.Component {
|
|||||||
</ShowMore>
|
</ShowMore>
|
||||||
<div className='float-right'>
|
<div className='float-right'>
|
||||||
<FlatButton
|
<FlatButton
|
||||||
className='control'
|
className='control cancel'
|
||||||
label='Cancel'
|
label='Cancel'
|
||||||
style={{
|
style={{
|
||||||
marginRight: 10
|
marginRight: 10
|
||||||
}}
|
}}
|
||||||
onClick={dispatcher('cancel')} />
|
onClick={dispatcher('cancel')} />
|
||||||
<RaisedButton
|
<RaisedButton
|
||||||
className='control'
|
className='control create-torrent'
|
||||||
label='Create Torrent'
|
label='Create Torrent'
|
||||||
primary
|
primary
|
||||||
onClick={this.handleSubmit} />
|
onClick={this.handleSubmit} />
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ const React = require('react')
|
|||||||
const Bitfield = require('bitfield')
|
const Bitfield = require('bitfield')
|
||||||
const prettyBytes = require('prettier-bytes')
|
const prettyBytes = require('prettier-bytes')
|
||||||
const zeroFill = require('zero-fill')
|
const zeroFill = require('zero-fill')
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const Playlist = require('../lib/playlist')
|
const Playlist = require('../lib/playlist')
|
||||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||||
module.exports = class Player extends React.Component {
|
module.exports = class Player extends React.Component {
|
||||||
@@ -26,6 +26,15 @@ module.exports = class Player extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onComponentWillUnmount () {
|
||||||
|
// Unload the media element so that Chromium stops trying to fetch data
|
||||||
|
const tag = document.querySelector('audio,video')
|
||||||
|
if (!tag) return
|
||||||
|
tag.pause()
|
||||||
|
tag.src = ''
|
||||||
|
tag.load()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles volume change by wheel
|
// Handles volume change by wheel
|
||||||
@@ -289,11 +298,8 @@ function renderCastScreen (state) {
|
|||||||
castType = 'DLNA'
|
castType = 'DLNA'
|
||||||
isCast = true
|
isCast = true
|
||||||
} else if (state.playing.location === 'external') {
|
} else if (state.playing.location === 'external') {
|
||||||
// TODO: get the player name in a more reliable way
|
|
||||||
const playerPath = state.saved.prefs.externalPlayerPath
|
|
||||||
const playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
|
|
||||||
castIcon = 'tv'
|
castIcon = 'tv'
|
||||||
castType = playerName
|
castType = state.getExternalPlayerName()
|
||||||
isCast = false
|
isCast = false
|
||||||
} else if (state.playing.location === 'error') {
|
} else if (state.playing.location === 'error') {
|
||||||
castIcon = 'error_outline'
|
castIcon = 'error_outline'
|
||||||
@@ -432,7 +438,7 @@ function renderPlayerControls (state) {
|
|||||||
]
|
]
|
||||||
|
|
||||||
if (state.playing.type === 'video') {
|
if (state.playing.type === 'video') {
|
||||||
// show closed captions icon
|
// Show closed captions icon
|
||||||
elements.push((
|
elements.push((
|
||||||
<i
|
<i
|
||||||
key='subtitles'
|
key='subtitles'
|
||||||
@@ -500,8 +506,6 @@ function renderPlayerControls (state) {
|
|||||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
'color-stop(' + (volume * 100) + '%, #727272))'
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: dcposch change the range input to use value / onChanged instead of
|
|
||||||
// "readonly" / onMouse[Down,Move,Up]
|
|
||||||
elements.push((
|
elements.push((
|
||||||
<div key='volume' className='volume float-left'>
|
<div key='volume' className='volume float-left'>
|
||||||
<i
|
<i
|
||||||
@@ -527,7 +531,7 @@ function renderPlayerControls (state) {
|
|||||||
</span>
|
</span>
|
||||||
))
|
))
|
||||||
|
|
||||||
// render playback rate
|
// Render playback rate
|
||||||
if (state.playing.playbackRate !== 1) {
|
if (state.playing.playbackRate !== 1) {
|
||||||
elements.push((
|
elements.push((
|
||||||
<span key='rate' className='rate float-left'>
|
<span key='rate' className='rate float-left'>
|
||||||
@@ -537,7 +541,9 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='controls' className='controls'>
|
<div key='controls' className='controls'
|
||||||
|
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
|
||||||
|
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
|
||||||
{elements}
|
{elements}
|
||||||
{renderCastOptions(state)}
|
{renderCastOptions(state)}
|
||||||
{renderSubtitleOptions(state)}
|
{renderSubtitleOptions(state)}
|
||||||
@@ -589,6 +595,8 @@ function renderPlayerControls (state) {
|
|||||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||||
// can be 'spongey' / non-contiguous
|
// can be 'spongey' / non-contiguous
|
||||||
function renderLoadingBar (state) {
|
function renderLoadingBar (state) {
|
||||||
|
if (config.IS_TEST) return // Don't integration test the loading bar. Screenshots won't match.
|
||||||
|
|
||||||
const torrentSummary = state.getPlayingTorrentSummary()
|
const torrentSummary = state.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary.progress) {
|
if (!torrentSummary.progress) {
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
const colors = require('material-ui/styles/colors')
|
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const React = require('react')
|
const React = require('react')
|
||||||
|
|
||||||
|
const colors = require('material-ui/styles/colors')
|
||||||
const Checkbox = require('material-ui/Checkbox').default
|
const Checkbox = require('material-ui/Checkbox').default
|
||||||
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
const Heading = require('../components/heading')
|
const Heading = require('../components/heading')
|
||||||
const PathSelector = require('../components/path-selector')
|
const PathSelector = require('../components/path-selector')
|
||||||
const RaisedButton = require('material-ui/RaisedButton').default
|
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
class PreferencesPage extends React.Component {
|
class PreferencesPage extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -21,6 +22,9 @@ class PreferencesPage extends React.Component {
|
|||||||
|
|
||||||
this.handleExternalPlayerPathChange =
|
this.handleExternalPlayerPathChange =
|
||||||
this.handleExternalPlayerPathChange.bind(this)
|
this.handleExternalPlayerPathChange.bind(this)
|
||||||
|
|
||||||
|
this.handleStartupChange =
|
||||||
|
this.handleStartupChange.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadPathSelector () {
|
downloadPathSelector () {
|
||||||
@@ -59,14 +63,12 @@ class PreferencesPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
externalPlayerPathSelector () {
|
externalPlayerPathSelector () {
|
||||||
const playerName = path.basename(
|
const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
|
||||||
this.props.state.unsaved.prefs.externalPlayerPath || 'VLC'
|
const playerName = this.props.state.getExternalPlayerName()
|
||||||
)
|
|
||||||
|
|
||||||
const description = this.props.state.unsaved.prefs.openExternalPlayer
|
const description = this.props.state.unsaved.prefs.openExternalPlayer
|
||||||
? `Torrent media files will always play in ${playerName}.`
|
? `Torrent media files will always play in ${playerName}.`
|
||||||
: `Torrent media files will play in ${playerName} if WebTorrent cannot ` +
|
: `Torrent media files will play in ${playerName} if WebTorrent cannot play them.`
|
||||||
'play them.'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Preference>
|
<Preference>
|
||||||
@@ -79,16 +81,12 @@ class PreferencesPage extends React.Component {
|
|||||||
displayValue={playerName}
|
displayValue={playerName}
|
||||||
onChange={this.handleExternalPlayerPathChange}
|
onChange={this.handleExternalPlayerPathChange}
|
||||||
title='External player'
|
title='External player'
|
||||||
value={this.props.state.unsaved.prefs.externalPlayerPath} />
|
value={playerPath ? path.dirname(playerPath) : null} />
|
||||||
</Preference>
|
</Preference>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleExternalPlayerPathChange (filePath) {
|
handleExternalPlayerPathChange (filePath) {
|
||||||
if (path.extname(filePath) === '.app') {
|
|
||||||
// Mac: Use executable in packaged .app bundle
|
|
||||||
filePath += '/Contents/MacOS/' + path.basename(filePath, '.app')
|
|
||||||
}
|
|
||||||
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +110,29 @@ class PreferencesPage extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleStartupChange (e, isChecked) {
|
||||||
|
dispatch('updatePreferences', 'startup', isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
setStartupSection () {
|
||||||
|
if (config.IS_PORTABLE) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreferencesSection title='Startup'>
|
||||||
|
<Preference>
|
||||||
|
<Checkbox
|
||||||
|
className='control'
|
||||||
|
checked={this.props.state.unsaved.prefs.startup}
|
||||||
|
label={'Open WebTorrent on startup.'}
|
||||||
|
onCheck={this.handleStartupChange}
|
||||||
|
/>
|
||||||
|
</Preference>
|
||||||
|
</PreferencesSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
handleSetDefaultApp () {
|
handleSetDefaultApp () {
|
||||||
dispatch('updatePreferences', 'isFileHandler', true)
|
dispatch('updatePreferences', 'isFileHandler', true)
|
||||||
}
|
}
|
||||||
@@ -134,6 +155,7 @@ class PreferencesPage extends React.Component {
|
|||||||
<PreferencesSection title='Default torrent app'>
|
<PreferencesSection title='Default torrent app'>
|
||||||
{this.setDefaultAppButton()}
|
{this.setDefaultAppButton()}
|
||||||
</PreferencesSection>
|
</PreferencesSection>
|
||||||
|
{this.setStartupSection()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +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 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 () {
|
||||||
@@ -46,7 +49,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
// Background image: show some nice visuals, like a frame from the movie, if possible
|
// Background image: show some nice visuals, like a frame from the movie, if possible
|
||||||
const style = {}
|
const style = {}
|
||||||
if (torrentSummary.posterFileName) {
|
if (torrentSummary.posterFileName) {
|
||||||
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
|
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
||||||
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
style.backgroundImage = `${gradient}, url('${posterPath}')`
|
style.backgroundImage = `${gradient}, url('${posterPath}')`
|
||||||
}
|
}
|
||||||
@@ -54,13 +57,12 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
// Foreground: name of the torrent, basic info like size, play button,
|
// Foreground: name of the torrent, basic info like size, play button,
|
||||||
// cast buttons if available, and delete
|
// cast buttons if available, and delete
|
||||||
const classes = ['torrent']
|
const classes = ['torrent']
|
||||||
// playStatus turns the play button into a loading spinner or error icon
|
|
||||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
|
||||||
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 Error('Missing torrentKey')
|
if (!torrentSummary.torrentKey) throw new InvalidTorrentError('Missing torrentKey')
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
|
||||||
key={torrentSummary.torrentKey}
|
key={torrentSummary.torrentKey}
|
||||||
style={style}
|
style={style}
|
||||||
className={classes.join(' ')}
|
className={classes.join(' ')}
|
||||||
@@ -69,6 +71,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
{this.renderTorrentMetadata(torrentSummary)}
|
{this.renderTorrentMetadata(torrentSummary)}
|
||||||
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
|
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
|
||||||
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
|
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
|
||||||
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -82,31 +85,66 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
|
|
||||||
// If it's downloading/seeding then show progress info
|
// If it's downloading/seeding then show progress info
|
||||||
const prog = torrentSummary.progress
|
const prog = torrentSummary.progress
|
||||||
|
let progElems
|
||||||
if (torrentSummary.error) {
|
if (torrentSummary.error) {
|
||||||
elements.push(
|
progElems = [getErrorMessage(torrentSummary)]
|
||||||
<div key='progress-info' className='ellipsis'>
|
|
||||||
{getErrorMessage(torrentSummary)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else if (torrentSummary.status !== 'paused' && prog) {
|
} else if (torrentSummary.status !== 'paused' && prog) {
|
||||||
elements.push(
|
progElems = [
|
||||||
<div key='progress-info' className='ellipsis'>
|
renderDownloadCheckbox(),
|
||||||
{renderProgressBar()}
|
renderTorrentStatus(),
|
||||||
{renderPercentProgress()}
|
renderProgressBar(),
|
||||||
{renderTotalProgress()}
|
renderPercentProgress(),
|
||||||
{renderPeers()}
|
renderTotalProgress(),
|
||||||
{renderDownloadSpeed()}
|
renderPeers(),
|
||||||
{renderUploadSpeed()}
|
renderSpeeds(),
|
||||||
{renderEta()}
|
renderEta()
|
||||||
</div>
|
]
|
||||||
)
|
} else {
|
||||||
|
progElems = [
|
||||||
|
renderDownloadCheckbox(),
|
||||||
|
renderTorrentStatus()
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
elements.push(
|
||||||
|
<div key='progress-info' className='ellipsis'>
|
||||||
|
{progElems}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (<div key='metadata' className='metadata'>{elements}</div>)
|
return (<div key='metadata' className='metadata'>{elements}</div>)
|
||||||
|
|
||||||
|
function renderDownloadCheckbox () {
|
||||||
|
const infoHash = torrentSummary.infoHash
|
||||||
|
const isActive = ['downloading', 'seeding'].includes(torrentSummary.status)
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
key='download-button'
|
||||||
|
className={'download ' + torrentSummary.status}
|
||||||
|
style={{display: 'inline-block', width: '32px'}}
|
||||||
|
iconStyle={{width: '20px', height: '20px'}}
|
||||||
|
checked={isActive}
|
||||||
|
onClick={stopPropagation}
|
||||||
|
onCheck={dispatcher('toggleTorrent', infoHash)} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function renderProgressBar () {
|
function renderProgressBar () {
|
||||||
const progress = Math.floor(100 * prog.progress)
|
const progress = Math.floor(100 * prog.progress)
|
||||||
return (<progress value={progress} max='100'>{progress}%</progress>)
|
const styles = {
|
||||||
|
wrapper: {
|
||||||
|
display: 'inline-block',
|
||||||
|
marginRight: '8px'
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
height: '8px',
|
||||||
|
width: '30px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={styles.wrapper}>
|
||||||
|
<LinearProgress style={styles.progress} mode='determinate' value={progress} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPercentProgress () {
|
function renderPercentProgress () {
|
||||||
@@ -130,14 +168,12 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
return (<span key='peers'>{prog.numPeers} {count}</span>)
|
return (<span key='peers'>{prog.numPeers} {count}</span>)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDownloadSpeed () {
|
function renderSpeeds () {
|
||||||
if (prog.downloadSpeed === 0) return
|
let str = ''
|
||||||
return (<span key='download'>↓ {prettyBytes(prog.downloadSpeed)}/s</span>)
|
if (prog.downloadSpeed > 0) str += ' ↓ ' + prettyBytes(prog.downloadSpeed) + '/s'
|
||||||
}
|
if (prog.uploadSpeed > 0) str += ' ↑ ' + prettyBytes(prog.uploadSpeed) + '/s'
|
||||||
|
if (str === '') return
|
||||||
function renderUploadSpeed () {
|
return (<span key='download'>{str}</span>)
|
||||||
if (prog.uploadSpeed === 0) return
|
|
||||||
return (<span key='upload'>↑ {prettyBytes(prog.uploadSpeed)}/s</span>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEta () {
|
function renderEta () {
|
||||||
@@ -158,7 +194,23 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
||||||
const secondsStr = seconds + 's'
|
const secondsStr = seconds + 's'
|
||||||
|
|
||||||
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
|
return (<span>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTorrentStatus () {
|
||||||
|
let status
|
||||||
|
if (torrentSummary.status === 'paused') {
|
||||||
|
if (!torrentSummary.progress) status = ''
|
||||||
|
else if (torrentSummary.progress.progress === 1) status = 'Not seeding'
|
||||||
|
else status = 'Paused'
|
||||||
|
} else if (torrentSummary.status === 'downloading') {
|
||||||
|
status = 'Downloading'
|
||||||
|
} else if (torrentSummary.status === 'seeding') {
|
||||||
|
status = 'Seeding'
|
||||||
|
} else { // torrentSummary.status is 'new' or something unexpected
|
||||||
|
status = ''
|
||||||
|
}
|
||||||
|
return (<span>{status}</span>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,69 +219,23 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
renderTorrentButtons (torrentSummary) {
|
renderTorrentButtons (torrentSummary) {
|
||||||
const infoHash = torrentSummary.infoHash
|
const infoHash = torrentSummary.infoHash
|
||||||
|
|
||||||
let playIcon, playTooltip, playClass
|
|
||||||
if (torrentSummary.playStatus === 'timeout') {
|
|
||||||
playIcon = 'warning'
|
|
||||||
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
|
||||||
} else {
|
|
||||||
playIcon = 'play_arrow'
|
|
||||||
playTooltip = 'Start streaming'
|
|
||||||
}
|
|
||||||
|
|
||||||
let downloadIcon, downloadTooltip
|
|
||||||
if (torrentSummary.status === 'seeding') {
|
|
||||||
downloadIcon = 'file_upload'
|
|
||||||
downloadTooltip = 'Seeding. Click to stop.'
|
|
||||||
} else if (torrentSummary.status === 'downloading') {
|
|
||||||
downloadIcon = 'file_download'
|
|
||||||
downloadTooltip = 'Torrenting. Click to stop.'
|
|
||||||
} else {
|
|
||||||
downloadIcon = 'file_download'
|
|
||||||
downloadTooltip = 'Click to start torrenting.'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only show the play/dowload buttons for torrents that contain playable media
|
// Only show the play/dowload buttons for torrents that contain playable media
|
||||||
let playButton, downloadButton, positionElem
|
let playButton
|
||||||
if (!torrentSummary.error) {
|
if (!torrentSummary.error && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||||
downloadButton = (
|
playButton = (
|
||||||
<i
|
<i
|
||||||
key='download-button'
|
key='play-button'
|
||||||
className={'button-round icon download ' + torrentSummary.status}
|
title='Start streaming'
|
||||||
title={downloadTooltip}
|
className={'icon play'}
|
||||||
onClick={dispatcher('toggleTorrent', infoHash)}>
|
onClick={dispatcher('playFile', infoHash)}>
|
||||||
{downloadIcon}
|
play_circle_outline
|
||||||
</i>
|
</i>
|
||||||
)
|
)
|
||||||
|
|
||||||
// Do we have a saved position? Show it using a radial progress bar on top
|
|
||||||
// of the play button, unless already showing a spinner there:
|
|
||||||
const willShowSpinner = torrentSummary.playStatus === 'requested'
|
|
||||||
const mostRecentFile = torrentSummary.files &&
|
|
||||||
torrentSummary.files[torrentSummary.mostRecentFileIndex]
|
|
||||||
if (mostRecentFile && mostRecentFile.currentTime && !willShowSpinner) {
|
|
||||||
const fraction = mostRecentFile.currentTime / mostRecentFile.duration
|
|
||||||
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
|
|
||||||
playClass = 'resume-position'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
|
||||||
playButton = (
|
|
||||||
<i
|
|
||||||
key='play-button'
|
|
||||||
title={playTooltip}
|
|
||||||
className={'button-round icon play ' + playClass}
|
|
||||||
onClick={dispatcher('playFile', infoHash)}>
|
|
||||||
{playIcon}
|
|
||||||
</i>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='buttons' className='buttons'>
|
<div className='torrent-controls'>
|
||||||
{positionElem}
|
|
||||||
{playButton}
|
{playButton}
|
||||||
{downloadButton}
|
|
||||||
<i
|
<i
|
||||||
key='delete-button'
|
key='delete-button'
|
||||||
className='icon delete'
|
className='icon delete'
|
||||||
@@ -345,7 +351,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
</td>
|
</td>
|
||||||
<td className='col-select'
|
<td className='col-select'
|
||||||
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
|
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||||
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
|
<i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
@@ -373,6 +379,10 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopPropagation (e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
function getErrorMessage (torrentSummary) {
|
function getErrorMessage (torrentSummary) {
|
||||||
const err = torrentSummary.error
|
const err = torrentSummary.error
|
||||||
if (err === 'path-missing') {
|
if (err === 'path-missing') {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const zeroFill = require('zero-fill')
|
|||||||
|
|
||||||
const crashReporter = require('../crash-reporter')
|
const crashReporter = require('../crash-reporter')
|
||||||
const config = require('../config')
|
const config = require('../config')
|
||||||
|
const {TorrentKeyNotFoundError} = require('./lib/errors')
|
||||||
const torrentPoster = require('./lib/torrent-poster')
|
const torrentPoster = require('./lib/torrent-poster')
|
||||||
|
|
||||||
// Report when the process crashes
|
// Report when the process crashes
|
||||||
@@ -53,11 +54,14 @@ const VERSION_STR = VERSION.match(/([0-9]+)/g)
|
|||||||
*/
|
*/
|
||||||
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'
|
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an ephemeral peer ID each time.
|
||||||
|
*/
|
||||||
|
const PEER_ID = Buffer.from(VERSION_PREFIX + crypto.randomBytes(9).toString('base64'))
|
||||||
|
|
||||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||||
// client, as explained here: https://webtorrent.io/faq
|
// client, as explained here: https://webtorrent.io/faq
|
||||||
const client = window.client = new WebTorrent({
|
let client = window.client = new WebTorrent({ peerId: PEER_ID })
|
||||||
peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
|
|
||||||
})
|
|
||||||
|
|
||||||
// WebTorrent-to-HTTP streaming sever
|
// WebTorrent-to-HTTP streaming sever
|
||||||
let server = null
|
let server = null
|
||||||
@@ -68,8 +72,7 @@ let prevProgress = null
|
|||||||
init()
|
init()
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
listenToClientEvents()
|
||||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
|
||||||
|
|
||||||
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
||||||
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
||||||
@@ -100,6 +103,11 @@ function init () {
|
|||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listenToClientEvents () {
|
||||||
|
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||||
|
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||||
|
}
|
||||||
|
|
||||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
|
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
|
||||||
// Returns a WebTorrent object. See https://git.io/vik9M
|
// Returns a WebTorrent object. See https://git.io/vik9M
|
||||||
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
||||||
@@ -156,7 +164,7 @@ function addTorrentEvents (torrent) {
|
|||||||
function torrentReady () {
|
function torrentReady () {
|
||||||
const info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipc.send('wt-ready', torrent.key, info)
|
ipc.send('wt-ready', torrent.key, info)
|
||||||
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
|
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
|
||||||
|
|
||||||
updateTorrentProgress()
|
updateTorrentProgress()
|
||||||
}
|
}
|
||||||
@@ -336,9 +344,11 @@ function getAudioMetadata (infoHash, index) {
|
|||||||
const torrent = client.get(infoHash)
|
const torrent = client.get(infoHash)
|
||||||
const file = torrent.files[index]
|
const file = torrent.files[index]
|
||||||
musicmetadata(file.createReadStream(), function (err, info) {
|
musicmetadata(file.createReadStream(), function (err, info) {
|
||||||
if (err) return
|
if (err) return console.log('error getting audio metadata for ' + infoHash + ':' + index, err)
|
||||||
console.log('got audio metadata for %s: %o', file.name, info)
|
const { artist, album, albumartist, title, year, track, disk, genre } = info
|
||||||
ipc.send('wt-audio-metadata', infoHash, index, info)
|
const importantInfo = { artist, album, albumartist, title, year, track, disk, genre }
|
||||||
|
console.log('got audio metadata for %s: %o', file.name, importantInfo)
|
||||||
|
ipc.send('wt-audio-metadata', infoHash, index, importantInfo)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,6 +360,9 @@ function selectFiles (torrentOrInfoHash, selections) {
|
|||||||
} else {
|
} else {
|
||||||
torrent = torrentOrInfoHash
|
torrent = torrentOrInfoHash
|
||||||
}
|
}
|
||||||
|
if (!torrent) {
|
||||||
|
throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
|
||||||
|
}
|
||||||
|
|
||||||
// Selections not specified?
|
// Selections not specified?
|
||||||
// Load all files. We still need to replace the default whole-torrent
|
// Load all files. We still need to replace the default whole-torrent
|
||||||
@@ -384,10 +397,24 @@ function selectFiles (torrentOrInfoHash, selections) {
|
|||||||
// Throws an Error if we're not currently torrenting anything w/ that key
|
// Throws an Error if we're not currently torrenting anything w/ that key
|
||||||
function getTorrent (torrentKey) {
|
function getTorrent (torrentKey) {
|
||||||
const ret = client.torrents.find((x) => x.key === torrentKey)
|
const ret = client.torrents.find((x) => x.key === torrentKey)
|
||||||
if (!ret) throw new Error('missing torrent key ' + torrentKey)
|
if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError (err) {
|
function onError (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove this once the following bugs are fixed:
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
|
||||||
|
// https://github.com/electron/electron/issues/7212
|
||||||
|
window.testOfflineMode = function () {
|
||||||
|
console.log('Test, going OFFLINE')
|
||||||
|
client = window.client = new WebTorrent({
|
||||||
|
peerId: PEER_ID,
|
||||||
|
tracker: false,
|
||||||
|
dht: false,
|
||||||
|
webSeeds: false
|
||||||
|
})
|
||||||
|
listenToClientEvents()
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<p>
|
<p>
|
||||||
Version <script>document.write(require('../package.json').version)</script>
|
Version <script>document.write(require('../package.json').version)</script>
|
||||||
(<script>document.write(require('webtorrent/package.json').version)</script>)
|
(<script>document.write(require('webtorrent/package.json').version)</script>)
|
||||||
|
(<script>document.write(process.arch === 'x64' ? '64-bit' : '32-bit')</script>)
|
||||||
</p>
|
</p>
|
||||||
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 22 KiB |
136
static/main.css
@@ -309,15 +309,6 @@ i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
|
|||||||
-webkit-filter: brightness(1.3);
|
-webkit-filter: brightness(1.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-round { /* Circular icon buttons, used on <i> tags */
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 22px;
|
|
||||||
transition: all 0.1s ease-out;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* INPUTS
|
* INPUTS
|
||||||
*/
|
*/
|
||||||
@@ -347,22 +338,18 @@ textarea,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent:not(:last-child) {
|
.torrent:not(:last-child) {
|
||||||
border-bottom: 1px solid rgb(20, 20, 20);
|
border-bottom: 1px solid #282828;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .metadata {
|
.torrent .metadata {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 25px;
|
top: 20px;
|
||||||
left: 15px;
|
left: 15px;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 30px);
|
||||||
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
|
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent:hover .metadata {
|
|
||||||
width: calc(100% - 150px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent .metadata span:not(:last-child)::after {
|
.torrent .metadata span:not(:last-child)::after {
|
||||||
content: ' • ';
|
content: ' • ';
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
@@ -370,79 +357,34 @@ textarea,
|
|||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .buttons {
|
.torrent:hover .metadata {
|
||||||
position: absolute;
|
width: calc(100% - 120px);
|
||||||
top: 29px;
|
}
|
||||||
right: 10px;
|
|
||||||
align-items: center;
|
.torrent .torrent-controls {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent:hover .buttons {
|
.torrent:hover .torrent-controls {
|
||||||
display: flex;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .buttons > * {
|
.torrent .play {
|
||||||
margin-left: 6px; /* space buttons apart */
|
position: absolute;
|
||||||
|
top: 23px;
|
||||||
|
right: 45px;
|
||||||
|
font-size: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .buttons .download {
|
.torrent .delete {
|
||||||
background-color: #2233BB;
|
position: absolute;
|
||||||
width: 28px;
|
top: 38px;
|
||||||
height: 28px;
|
right: 12px;
|
||||||
border-radius: 14px;
|
|
||||||
font-size: 18px;
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .buttons .download.downloading {
|
.torrent .download {
|
||||||
color: #44dd44;
|
vertical-align: -0.4em;
|
||||||
}
|
margin-left: -2px;
|
||||||
|
|
||||||
.torrent .buttons .download.seeding {
|
|
||||||
color: #44dd44;
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent .buttons .play {
|
|
||||||
padding-top: 10px;
|
|
||||||
background-color: #F44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent.timeout .play {
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent.requested .play,
|
|
||||||
.loading-spinner {
|
|
||||||
border-top: 6px solid rgba(255, 255, 255, 0.2);
|
|
||||||
border-right: 6px solid rgba(255, 255, 255, 0.2);
|
|
||||||
border-bottom: 6px solid rgba(255, 255, 255, 0.2);
|
|
||||||
border-left: 6px solid #ffffff;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: transparent;
|
|
||||||
animation: load8 1.1s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes load8 {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent .buttons .play.resume-position {
|
|
||||||
position: relative;
|
|
||||||
-webkit-clip-path: circle(18px at center);
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent .buttons .delete {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent .buttons .delete:hover {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .buttons .radial-progress {
|
.torrent .buttons .radial-progress {
|
||||||
@@ -450,23 +392,27 @@ textarea,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent .name {
|
.torrent .name {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress {
|
.torrent hr {
|
||||||
width: 60px;
|
position: absolute;
|
||||||
margin-right: 8px;
|
bottom: 5px;
|
||||||
-webkit-appearance: none;
|
left: calc(50% - 20px);
|
||||||
|
width: 40px;
|
||||||
|
height: 1px;
|
||||||
|
background: #ccc;
|
||||||
|
display: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress::-webkit-progress-bar {
|
.torrent:hover hr {
|
||||||
background-color: #888;
|
display: block;
|
||||||
}
|
|
||||||
|
|
||||||
progress::-webkit-progress-value {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -690,14 +636,6 @@ body.drag .app::after {
|
|||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: find better way to handle this (that also
|
|
||||||
* keeps the header visible too).
|
|
||||||
*/
|
|
||||||
.app.hide-video-controls .player .controls:hover {
|
|
||||||
opacity: 1;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* invisible click target for scrubbing */
|
/* invisible click target for scrubbing */
|
||||||
.player .controls .scrub-bar {
|
.player .controls .scrub-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>WebTorrent Desktop</title>
|
<title>Main Window</title>
|
||||||
<link rel="stylesheet" href="main.css">
|
<link rel="stylesheet" href="main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>WebTorrent Desktop</title>
|
<title>WebTorrent Hidden Window</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: #282828;
|
background-color: #282828;
|
||||||
|
|||||||
15
test/config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const TEMP_DIR = process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp'
|
||||||
|
const TEST_DIR = path.join(TEMP_DIR, 'WebTorrentTest')
|
||||||
|
const TEST_DIR_DOWNLOAD = path.join(TEST_DIR, 'Downloads')
|
||||||
|
const TEST_DIR_DESKTOP = path.join(TEST_DIR, 'Desktop')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
TORRENT_FILES: [path.join(__dirname, 'resources', '1.torrent')],
|
||||||
|
SEED_FILES: [path.join(TEST_DIR_DESKTOP, 'tmp.jpg')],
|
||||||
|
SAVED_TORRENT_FILE: path.join(TEST_DIR_DESKTOP, 'saved.torrent'),
|
||||||
|
TEST_DIR,
|
||||||
|
TEST_DIR_DOWNLOAD,
|
||||||
|
TEST_DIR_DESKTOP
|
||||||
|
}
|
||||||
19
test/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const test = require('tape')
|
||||||
|
const setup = require('./setup')
|
||||||
|
|
||||||
|
test.onFinish(setup.deleteTestDataDir)
|
||||||
|
|
||||||
|
test('app runs', function (t) {
|
||||||
|
t.timeoutAfter(10e3)
|
||||||
|
setup.resetTestDataDir()
|
||||||
|
const app = setup.createApp()
|
||||||
|
setup.waitForLoad(app, t)
|
||||||
|
.then(() => setup.screenshotCreateOrCompare(app, t, 'app-basic'))
|
||||||
|
.then(() => setup.endTest(app, t),
|
||||||
|
(err) => setup.endTest(app, t, err || 'error'))
|
||||||
|
})
|
||||||
|
|
||||||
|
require('./test-torrent-list')
|
||||||
|
require('./test-add-torrent')
|
||||||
|
require('./test-video')
|
||||||
|
require('./test-audio')
|
||||||
15
test/mocks.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const electron = require('electron')
|
||||||
|
const config = require('./config')
|
||||||
|
|
||||||
|
console.log('Mocking electron.dialog.showOpenDialog...')
|
||||||
|
electron.dialog.showOpenDialog = function (win, opts, cb) {
|
||||||
|
const ret = /select.*torrent file/i.test(opts.title)
|
||||||
|
? config.TORRENT_FILES
|
||||||
|
: config.SEED_FILES
|
||||||
|
cb(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Mocking electron.remote.dialog.showSaveDialog...')
|
||||||
|
electron.dialog.showSaveDialog = function (win, opts, cb) {
|
||||||
|
cb(config.SAVED_TORRENT_FILE)
|
||||||
|
}
|
||||||
BIN
test/resources/1.torrent
Normal file
BIN
test/resources/expected-single-file.torrent
Normal file
BIN
test/resources/m3.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
test/resources/monitor-test.mp4
Normal file
BIN
test/screenshots/darwin/add-torrent-0-percent.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/add-torrent-100-percent.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
test/screenshots/darwin/app-basic.png
Normal file
|
After Width: | Height: | Size: 1023 KiB |
BIN
test/screenshots/darwin/create-torrent-100-percent.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
test/screenshots/darwin/create-torrent-advanced.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
test/screenshots/darwin/create-torrent-simple.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
test/screenshots/darwin/play-torrent-bbb.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
test/screenshots/darwin/play-torrent-return.png
Normal file
|
After Width: | Height: | Size: 710 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-2.png
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-3.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-4.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-5.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
test/screenshots/darwin/play-torrent-wired-fullscreen.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/play-torrent-wired-list.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
test/screenshots/darwin/play-torrent-wired.png
Normal file
|
After Width: | Height: | Size: 481 KiB |
BIN
test/screenshots/darwin/prefs-basic.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-delete-data.png
Normal file
|
After Width: | Height: | Size: 701 KiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-deleted.png
Normal file
|
After Width: | Height: | Size: 932 KiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-expand-deselect.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-expand-start.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-expand.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/torrent-list-cosmos-hover.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
test/screenshots/darwin/torrent-list-delete-prompt.png
Normal file
|
After Width: | Height: | Size: 617 KiB |
BIN
test/screenshots/darwin/torrent-list-delete.png
Normal file
|
After Width: | Height: | Size: 617 KiB |
BIN
test/screenshots/darwin/torrent-list-deleted.png
Normal file
|
After Width: | Height: | Size: 777 KiB |
BIN
test/screenshots/darwin/torrent-list-download-path-missing.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
test/screenshots/darwin/torrent-list-hover-download.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/torrent-list-hover.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
test/screenshots/darwin/torrent-list-start-download.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/darwin/torrent-list-stop-download.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
test/screenshots/win32/add-torrent-0-percent.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
test/screenshots/win32/add-torrent-100-percent.png
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
test/screenshots/win32/app-basic.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
test/screenshots/win32/create-torrent-100-percent.png
Normal file
|
After Width: | Height: | Size: 422 KiB |
BIN
test/screenshots/win32/create-torrent-advanced.png
Normal file
|
After Width: | Height: | Size: 43 KiB |