Compare commits
175 Commits
dc/vlc
...
entitlemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a9ba11b6 | ||
|
|
b367c0b72e | ||
|
|
bf3b9ced74 | ||
|
|
9ecc12fb7f | ||
|
|
aafb1421c6 | ||
|
|
76c732bafb | ||
|
|
ab476c9a9c | ||
|
|
4470310814 | ||
|
|
b6ba4f45c8 | ||
|
|
84c860cfcb | ||
|
|
47c554a5ff | ||
|
|
4e46b16c13 | ||
|
|
22cdcdb468 | ||
|
|
f238b2d105 | ||
|
|
3a81799828 | ||
|
|
5dca89b61c | ||
|
|
264c035ef7 | ||
|
|
8f39f8a23e | ||
|
|
a29dbd7a71 | ||
|
|
60a8969abc | ||
|
|
9747d28514 | ||
|
|
17ccd217a9 | ||
|
|
0df6198549 | ||
|
|
74ada99f2b | ||
|
|
81d5a367da | ||
|
|
189e4bdc24 | ||
|
|
7bd30f8a16 | ||
|
|
7c6b7e4a6d | ||
|
|
fe50f76619 | ||
|
|
973a366b94 | ||
|
|
b0116deb35 | ||
|
|
511382d384 | ||
|
|
cfb3a01239 | ||
|
|
736d575ab1 | ||
|
|
34a9508483 | ||
|
|
21ed8797c2 | ||
|
|
454491572a | ||
|
|
6518a1535c | ||
|
|
0095687bf5 | ||
|
|
d466ed085a | ||
|
|
eeda7c17c5 | ||
|
|
b89deb46db | ||
|
|
951a89c6c9 | ||
|
|
d4e6c84279 | ||
|
|
9731d85ca3 | ||
|
|
98f7ba8931 | ||
|
|
24c775608e | ||
|
|
f4eab12c3f | ||
|
|
8eeddeb4bc | ||
|
|
58f1594d9e | ||
|
|
c126ac0a84 | ||
|
|
6768be710e | ||
|
|
b63aa090dc | ||
|
|
05ef8be5bc | ||
|
|
1a09249bc3 | ||
|
|
803820dfca | ||
|
|
deb111bf62 | ||
|
|
7d64c7e308 | ||
|
|
ffb7183f51 | ||
|
|
20c6737aba | ||
|
|
959fb20b61 | ||
|
|
5d14c923fa | ||
|
|
5ffa7c4465 | ||
|
|
461744da5b | ||
|
|
6df33bc58b | ||
|
|
b5ae8f56cf | ||
|
|
2e0de52520 | ||
|
|
7b1ff0efc6 | ||
|
|
4002392b7f | ||
|
|
ee4b84fc11 | ||
|
|
90a0ce4a4d | ||
|
|
80faba8234 | ||
|
|
ac0574a473 | ||
|
|
792e3430f1 | ||
|
|
9e33be0ab1 | ||
|
|
c343c008ed | ||
|
|
6405be5144 | ||
|
|
db743daae5 | ||
|
|
290a25c393 | ||
|
|
6589e134b3 | ||
|
|
a2aa5e4271 | ||
|
|
205e2eb551 | ||
|
|
53209a9da3 | ||
|
|
2a23611c5f | ||
|
|
cb71913cbe | ||
|
|
836d7c6664 | ||
|
|
4cef9f2911 | ||
|
|
0913988d53 | ||
|
|
6468f82a7f | ||
|
|
fd0fc769b1 | ||
|
|
e5b648dfc6 | ||
|
|
7701c5f097 | ||
|
|
e5eddce868 | ||
|
|
72f917a744 | ||
|
|
0b82c83d44 | ||
|
|
602654cc1d | ||
|
|
350bed53a3 | ||
|
|
840754fb59 | ||
|
|
ed46583226 | ||
|
|
93252d430e | ||
|
|
bfd09a058e | ||
|
|
b1a7543d37 | ||
|
|
39195fe8c4 | ||
|
|
ea1c66b3fc | ||
|
|
f35eb73d50 | ||
|
|
c99af4718e | ||
|
|
dbef07e334 | ||
|
|
969ad64c47 | ||
|
|
5dd5e8661b | ||
|
|
5c9265fc99 | ||
|
|
1deab08d38 | ||
|
|
3d6da99e8e | ||
|
|
2005ee4d0b | ||
|
|
c99da2ccaa | ||
|
|
4bffb6634c | ||
|
|
504aca747d | ||
|
|
2085312c34 | ||
|
|
744d38259e | ||
|
|
868739445a | ||
|
|
98d8a798ce | ||
|
|
fe31cfaa3e | ||
|
|
17d5490448 | ||
|
|
d4c415d585 | ||
|
|
cb8f7f53c2 | ||
|
|
8d93641ebe | ||
|
|
4faf30e0a1 | ||
|
|
ed1b27ede0 | ||
|
|
252443a529 | ||
|
|
86f5a1a54e | ||
|
|
0b1872fa28 | ||
|
|
9eeb8133af | ||
|
|
1eb5504029 | ||
|
|
dfe8c3eb6b | ||
|
|
2b8c1fe709 | ||
|
|
905cc527d0 | ||
|
|
95019453fd | ||
|
|
e46a7f42df | ||
|
|
15a59f445b | ||
|
|
dea951fc42 | ||
|
|
347eb2c7f0 | ||
|
|
4221883eb4 | ||
|
|
27f729250f | ||
|
|
452bbb60c4 | ||
|
|
9d4aeaedd3 | ||
|
|
558b6c1648 | ||
|
|
98e263e69a | ||
|
|
18b126e0d2 | ||
|
|
82dff65572 | ||
|
|
d60d298b8f | ||
|
|
ffbd8184b5 | ||
|
|
11cf4aeecd | ||
|
|
b0b8b56816 | ||
|
|
967e5ecb9c | ||
|
|
f0315f7f77 | ||
|
|
facb07cbb1 | ||
|
|
41910aea9c | ||
|
|
8fcfa3b97a | ||
|
|
8ebb2349dd | ||
|
|
1e487a3c2a | ||
|
|
291ea94a10 | ||
|
|
ade6c1e4a0 | ||
|
|
bde5dc14c3 | ||
|
|
0a005eb054 | ||
|
|
735851486e | ||
|
|
56ba5c705a | ||
|
|
cdab2dbc65 | ||
|
|
4284eb8f75 | ||
|
|
2707fc9053 | ||
|
|
1d4d4319e4 | ||
|
|
c5cc0ce09d | ||
|
|
fdd7dab76f | ||
|
|
7624f2da98 | ||
|
|
ef51f827dc | ||
|
|
011ab13c83 | ||
|
|
017d61815f |
@@ -11,5 +11,12 @@
|
|||||||
- Dan Flettre <fletd01@yahoo.com>
|
- Dan Flettre <fletd01@yahoo.com>
|
||||||
- Liam Gray <liam.r.gray@gmail.com>
|
- Liam Gray <liam.r.gray@gmail.com>
|
||||||
- grunjol <grunjol@argenteam.net>
|
- grunjol <grunjol@argenteam.net>
|
||||||
|
- Rémi Jouannet <remijouannet@users.noreply.github.com>
|
||||||
|
- Evan Miller <miller.evan815@gmail.com>
|
||||||
|
- Alex <alxmorais8@msn.com>
|
||||||
|
- Diego Rodríguez Baquero <diegorbaquero@gmail.com>
|
||||||
|
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
|
||||||
|
- gabriel <furstenheim@gmail.com>
|
||||||
|
- Rolando Guedes <rolando.guedes@3gnt.net>
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
89
CHANGELOG.md
89
CHANGELOG.md
@@ -1,16 +1,89 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
## UNRELEASED
|
## v0.5.1 - 2016-05-18
|
||||||
|
|
||||||
### Added
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Use Squirrel.Windows 1.3.0
|
|
||||||
- Fix installing when the app is already installed
|
|
||||||
- Don't kill unrelated processes on uninstall
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fix auto-updater (OS X, Windows).
|
||||||
|
|
||||||
|
## v0.5.0 - 2016-05-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Select/deselect individual files to torrent.
|
||||||
|
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
|
||||||
|
- "Add Subtitle File..." menu item.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- When manually adding subtitle track(s), always switch to the new track.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Magnet links throw exception on app launch. (OS X)
|
||||||
|
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
|
||||||
|
- Missing 'About WebTorrent' menu item. (Windows)
|
||||||
|
- Rare exception. ("Cannot create BrowserWindow before app is ready")
|
||||||
|
|
||||||
|
## v0.4.0 - 2016-05-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Better Windows support!
|
||||||
|
- Windows 32-bit build.
|
||||||
|
- Windows Portable App build.
|
||||||
|
- Windows app signing, for fewer install warnings.
|
||||||
|
- Better Linux support!
|
||||||
|
- Linux 32-bit build.
|
||||||
|
- Subtitles support!
|
||||||
|
- .srt and .vtt file support.
|
||||||
|
- Drag-and-drop files on video, or choose from file selector.
|
||||||
|
- Multiple subtitle files support.
|
||||||
|
- Stream to VLC when the audio codec is unplayable (e.g. AC3, EAC3).
|
||||||
|
- "Show in Folder" item in context menu.
|
||||||
|
- Volume slider, with mute/unmute button.
|
||||||
|
- New "Create torrent" page to modify:
|
||||||
|
- Torrent comment.
|
||||||
|
- Trackers.
|
||||||
|
- Private torrent flag.
|
||||||
|
- Use mouse wheel to increase/decrease volume.
|
||||||
|
- Bounce the Downloads stack when download completes. (OS X)
|
||||||
|
- New default torrent on first launch: The WIRED CD.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improve app startup time by 40%.
|
||||||
|
- UI tweaks: Reduce font size, reduce torrent list item height.
|
||||||
|
- Add Playback menu for playback-related functionality.
|
||||||
|
- Fix installing when the app is already installed. (Windows)
|
||||||
|
- Don't kill unrelated processes on uninstall. (Windows)
|
||||||
|
- Set "sheet offset" correctly for create torrent dialog. (OS X)
|
||||||
|
- Remove OS X-style Window menu. (Linux, Windows)
|
||||||
|
- Remove "Add Fake Airplay/Chromecast" menu items.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is
|
||||||
|
temporary. (OS X)
|
||||||
|
- When fullscreen, make controls use the full window. (OS X)
|
||||||
|
- Support creating torrents that contain .torrent files.
|
||||||
|
- Block power save while casting to a remote device.
|
||||||
|
- Do not block power save when the space key is pressed from the torrent list.
|
||||||
|
- Support playing .mpg and .ogv extensions in the app.
|
||||||
|
- Fix video centering for multi-screen setups.
|
||||||
|
- Show an error when adding a duplicate torrent.
|
||||||
|
- Show an error when adding an invalid magnet link.
|
||||||
|
- Do not stop music when tabbing to another program (OS X)
|
||||||
|
- Properly size the Windows volume mixer icon.
|
||||||
|
- Default to the user's OS-defined, localized "Downloads" folder.
|
||||||
|
- Enforce minimimum window size when resizing player to prevent window disappearing.
|
||||||
|
- Fix rare race condition error on app quit.
|
||||||
|
- Don't use zero-byte torrent "poster" images.
|
||||||
|
|
||||||
|
Thanks to @grunjol, @rguedes, @furstenheim, @karloluis, @DiegoRBaquero, @alxhotel,
|
||||||
|
@AgentEpsilon, @remijouannet, Rolando Guedes, @dcposch, and @feross for contributing
|
||||||
|
to this release!
|
||||||
|
|
||||||
## v0.3.3 - 2016-04-07
|
## v0.3.3 - 2016-04-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) Feross Aboukhadijeh
|
Copyright (c) WebTorrent, LLC
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -87,4 +87,4 @@ brew install wine
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org).
|
MIT. Copyright (c) [WebTorrent, LLC](https://webtorrent.io).
|
||||||
|
|||||||
51
bin/check-deps.js
Executable file
51
bin/check-deps.js
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
var fs = require('fs')
|
||||||
|
var cp = require('child_process')
|
||||||
|
|
||||||
|
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen']
|
||||||
|
var EXECUTABLE_DEPS = ['gh-release', 'standard']
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
// Scans our codebase and package.json for missing or unused dependencies
|
||||||
|
// Process returns 0 on success, prints a message and returns 1 on failure
|
||||||
|
function main () {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
console.log('Sorry, check-deps only works on Mac and Linux')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsDeps = findJSDeps()
|
||||||
|
var packageDeps = findPackageDeps()
|
||||||
|
|
||||||
|
var missingDeps = jsDeps.filter((dep) =>
|
||||||
|
packageDeps.indexOf(dep) < 0 &&
|
||||||
|
BUILT_IN_DEPS.indexOf(dep) < 0)
|
||||||
|
var unusedDeps = packageDeps.filter((dep) =>
|
||||||
|
jsDeps.indexOf(dep) < 0 &&
|
||||||
|
EXECUTABLE_DEPS.indexOf(dep) < 0)
|
||||||
|
|
||||||
|
if (missingDeps.length > 0) console.log('Missing package dependencies: ' + missingDeps)
|
||||||
|
if (unusedDeps.length > 0) console.log('Unused package dependencies: ' + unusedDeps)
|
||||||
|
|
||||||
|
if (missingDeps.length + unusedDeps.length > 0) process.exit(1)
|
||||||
|
|
||||||
|
console.log('Lookin good!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds all dependencies, required, optional, or dev, in package.json
|
||||||
|
function findPackageDeps () {
|
||||||
|
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||||
|
var requiredDeps = Object.keys(pkg.dependencies)
|
||||||
|
var devDeps = Object.keys(pkg.devDependencies)
|
||||||
|
var optionalDeps = Object.keys(pkg.optionalDependencies)
|
||||||
|
|
||||||
|
return [].concat(requiredDeps, devDeps, optionalDeps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds all dependencies required() in the code
|
||||||
|
function findJSDeps () {
|
||||||
|
var stdout = cp.execSync('./bin/list-deps.sh')
|
||||||
|
return stdout.toString().trim().split('\n')
|
||||||
|
}
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
* Useful for developers.
|
* Useful for developers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var fs = require('fs')
|
||||||
var os = require('os')
|
var os = require('os')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var pathExists = require('path-exists')
|
|
||||||
var rimraf = require('rimraf')
|
var rimraf = require('rimraf')
|
||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
@@ -15,7 +15,12 @@ var handlers = require('../main/handlers')
|
|||||||
|
|
||||||
rimraf.sync(config.CONFIG_PATH)
|
rimraf.sync(config.CONFIG_PATH)
|
||||||
|
|
||||||
var tmpPath = path.join(pathExists.sync('/tmp') ? '/tmp' : os.tmpDir(), 'webtorrent')
|
var tmpPath
|
||||||
|
try {
|
||||||
|
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
|
||||||
|
} catch (err) {
|
||||||
|
tmpPath = path.join(os.tmpDir(), 'webtorrent')
|
||||||
|
}
|
||||||
rimraf.sync(tmpPath)
|
rimraf.sync(tmpPath)
|
||||||
|
|
||||||
// Uninstall .torrent file and magnet link handlers
|
// Uninstall .torrent file and magnet link handlers
|
||||||
|
|||||||
10
bin/list-deps.sh
Executable file
10
bin/list-deps.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# This is a truly heinous hack, but it works pretty nicely.
|
||||||
|
# Find all modules we're requiring---even conditional requires.
|
||||||
|
|
||||||
|
grep "require('" *.js bin/ main/ renderer/ -R |
|
||||||
|
grep '.js:' |
|
||||||
|
sed "s/.*require('\([^'\/]*\).*/\1/" |
|
||||||
|
grep -v '^\.' |
|
||||||
|
sort |
|
||||||
|
uniq
|
||||||
8
bin/open-config.js
Executable file
8
bin/open-config.js
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
var open = require('open')
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
|
var configPath = path.join(config.CONFIG_PATH, 'config.json')
|
||||||
|
open(configPath)
|
||||||
@@ -9,6 +9,7 @@ var electronPackager = require('electron-packager')
|
|||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var minimist = require('minimist')
|
var minimist = require('minimist')
|
||||||
var mkdirp = require('mkdirp')
|
var mkdirp = require('mkdirp')
|
||||||
|
var os = require('os')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var rimraf = require('rimraf')
|
var rimraf = require('rimraf')
|
||||||
var series = require('run-series')
|
var series = require('run-series')
|
||||||
@@ -18,16 +19,6 @@ var config = require('../config')
|
|||||||
var pkg = require('../package.json')
|
var pkg = require('../package.json')
|
||||||
|
|
||||||
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
||||||
|
|
||||||
/*
|
|
||||||
* Path to folder with the following files:
|
|
||||||
* - Windows Authenticode private key and cert (authenticode.p12)
|
|
||||||
* - Windows Authenticode password file (authenticode.txt)
|
|
||||||
*/
|
|
||||||
var CERT_PATH = process.platform === 'win32'
|
|
||||||
? 'D:'
|
|
||||||
: '/Volumes/Certs'
|
|
||||||
|
|
||||||
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
||||||
|
|
||||||
var argv = minimist(process.argv.slice(2), {
|
var argv = minimist(process.argv.slice(2), {
|
||||||
@@ -64,9 +55,6 @@ function build () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var all = {
|
var all = {
|
||||||
// Build 64 bit binaries only.
|
|
||||||
arch: 'x64',
|
|
||||||
|
|
||||||
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
|
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
|
||||||
// property on Windows, and `NSHumanReadableCopyright` on OS X.
|
// property on Windows, and `NSHumanReadableCopyright` on OS X.
|
||||||
'app-copyright': config.APP_COPYRIGHT,
|
'app-copyright': config.APP_COPYRIGHT,
|
||||||
@@ -85,9 +73,9 @@ var all = {
|
|||||||
'asar-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 OS X. We're using the short git hash (e.g. 'e7d837e')
|
// Windows, and CFBundleVersion on OS X. Note: Windows requires the build version to
|
||||||
// Windows requires the build version to start with a number :/ so we stick on a prefix
|
// start with a number. We're using the version of the underlying WebTorrent library.
|
||||||
'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''),
|
'build-version': require('webtorrent/package.json').version,
|
||||||
|
|
||||||
// The application source directory.
|
// The application source directory.
|
||||||
dir: config.ROOT_PATH,
|
dir: config.ROOT_PATH,
|
||||||
@@ -110,11 +98,15 @@ var all = {
|
|||||||
prune: true,
|
prune: true,
|
||||||
|
|
||||||
// The Electron version with which the app is built (without the leading 'v')
|
// The Electron version with which the app is built (without the leading 'v')
|
||||||
version: pkg.dependencies['electron-prebuilt']
|
version: require('electron-prebuilt/package.json').version
|
||||||
}
|
}
|
||||||
|
|
||||||
var darwin = {
|
var darwin = {
|
||||||
platform: 'darwin',
|
// Build for OS X
|
||||||
|
platform: 'mas',
|
||||||
|
|
||||||
|
// Build 64 bit binaries only.
|
||||||
|
arch: 'x64',
|
||||||
|
|
||||||
// The bundle identifier to use in the application's plist (OS X only).
|
// The bundle identifier to use in the application's plist (OS X only).
|
||||||
'app-bundle-id': 'io.webtorrent.webtorrent',
|
'app-bundle-id': 'io.webtorrent.webtorrent',
|
||||||
@@ -131,8 +123,12 @@ var darwin = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var win32 = {
|
var win32 = {
|
||||||
|
// Build for Windows.
|
||||||
platform: 'win32',
|
platform: 'win32',
|
||||||
|
|
||||||
|
// Build 32 bit binaries only.
|
||||||
|
arch: 'ia32',
|
||||||
|
|
||||||
// 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': {
|
'version-string': {
|
||||||
|
|
||||||
@@ -161,9 +157,10 @@ var win32 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var linux = {
|
var linux = {
|
||||||
|
// Build for Linux.
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
|
|
||||||
// Build 32/64 bit binaries.
|
// Build 32 and 64 bit 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.
|
||||||
@@ -177,7 +174,7 @@ function buildDarwin (cb) {
|
|||||||
console.log('OS X: Packaging electron...')
|
console.log('OS X: Packaging electron...')
|
||||||
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log('OS X: Packaged electron. ' + buildPath[0])
|
console.log('OS X: Packaged electron. ' + buildPath)
|
||||||
|
|
||||||
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||||
var contentsPath = path.join(appPath, 'Contents')
|
var contentsPath = path.join(appPath, 'Contents')
|
||||||
@@ -214,6 +211,8 @@ function buildDarwin (cb) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
infoPlist.ElectronTeamID = '5MAMC8G3L8'
|
||||||
|
|
||||||
fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
|
fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
|
||||||
|
|
||||||
// Copy torrent file icon into app bundle
|
// Copy torrent file icon into app bundle
|
||||||
@@ -251,8 +250,9 @@ function buildDarwin (cb) {
|
|||||||
*/
|
*/
|
||||||
var signOpts = {
|
var signOpts = {
|
||||||
app: appPath,
|
app: appPath,
|
||||||
platform: 'darwin',
|
entitlements: path.join(config.STATIC_PATH, 'parent.entitlements'),
|
||||||
verbose: true
|
'entitlements-inherit': path.join(config.STATIC_PATH, 'child.entitlements'),
|
||||||
|
platform: 'mas'
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('OS X: Signing app...')
|
console.log('OS X: Signing app...')
|
||||||
@@ -277,7 +277,7 @@ function buildDarwin (cb) {
|
|||||||
|
|
||||||
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
|
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
|
||||||
zip(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log('OS X: Created zip.')
|
console.log('OS X: Created zip.')
|
||||||
}
|
}
|
||||||
@@ -327,11 +327,24 @@ function buildDarwin (cb) {
|
|||||||
|
|
||||||
function buildWin32 (cb) {
|
function buildWin32 (cb) {
|
||||||
var installer = require('electron-winstaller')
|
var installer = require('electron-winstaller')
|
||||||
|
|
||||||
console.log('Windows: Packaging electron...')
|
console.log('Windows: Packaging electron...')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path to folder with the following files:
|
||||||
|
* - Windows Authenticode private key and cert (authenticode.p12)
|
||||||
|
* - Windows Authenticode password file (authenticode.txt)
|
||||||
|
*/
|
||||||
|
var CERT_PATH
|
||||||
|
try {
|
||||||
|
fs.accessSync('D:')
|
||||||
|
CERT_PATH = 'D:'
|
||||||
|
} catch (err) {
|
||||||
|
CERT_PATH = path.join(os.homedir(), 'Desktop')
|
||||||
|
}
|
||||||
|
|
||||||
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log('Windows: Packaged electron. ' + buildPath[0])
|
console.log('Windows: Packaged electron. ' + buildPath)
|
||||||
|
|
||||||
var signWithParams
|
var signWithParams
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
@@ -358,6 +371,7 @@ function buildWin32 (cb) {
|
|||||||
|
|
||||||
function packageInstaller (cb) {
|
function packageInstaller (cb) {
|
||||||
console.log('Windows: Creating installer...')
|
console.log('Windows: Creating installer...')
|
||||||
|
|
||||||
installer.createWindowsInstaller({
|
installer.createWindowsInstaller({
|
||||||
appDirectory: buildPath[0],
|
appDirectory: buildPath[0],
|
||||||
authors: config.APP_TEAM,
|
authors: config.APP_TEAM,
|
||||||
@@ -376,14 +390,15 @@ function buildWin32 (cb) {
|
|||||||
title: config.APP_NAME,
|
title: config.APP_NAME,
|
||||||
usePackageJson: false,
|
usePackageJson: false,
|
||||||
version: pkg.version
|
version: pkg.version
|
||||||
}).then(function () {
|
})
|
||||||
|
.then(function () {
|
||||||
console.log('Windows: Created installer.')
|
console.log('Windows: Created installer.')
|
||||||
cb(null)
|
cb(null)
|
||||||
}).catch(cb)
|
})
|
||||||
|
.catch(cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
function packagePortable (cb) {
|
function packagePortable (cb) {
|
||||||
// Create Windows portable app
|
|
||||||
console.log('Windows: Creating portable app...')
|
console.log('Windows: Creating portable app...')
|
||||||
|
|
||||||
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
||||||
@@ -391,7 +406,7 @@ function buildWin32 (cb) {
|
|||||||
|
|
||||||
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
||||||
zip(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log('Windows: Created portable app.')
|
console.log('Windows: Created portable app.')
|
||||||
cb(null)
|
cb(null)
|
||||||
@@ -403,7 +418,7 @@ function buildLinux (cb) {
|
|||||||
console.log('Linux: Packaging electron...')
|
console.log('Linux: Packaging electron...')
|
||||||
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log('Linux: Packaged electron. ' + buildPath[0])
|
console.log('Linux: Packaged electron. ' + buildPath)
|
||||||
|
|
||||||
var tasks = []
|
var tasks = []
|
||||||
buildPath.forEach(function (filesPath) {
|
buildPath.forEach(function (filesPath) {
|
||||||
@@ -455,7 +470,7 @@ function buildLinux (cb) {
|
|||||||
|
|
||||||
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-' + destArch + '.zip')
|
||||||
zip(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log(`Linux: Created ${destArch} zip.`)
|
console.log(`Linux: Created ${destArch} zip.`)
|
||||||
cb(null)
|
cb(null)
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ npm run package -- --sign
|
|||||||
git push
|
git push
|
||||||
git push --tags
|
git push --tags
|
||||||
npm publish
|
npm publish
|
||||||
gh-release
|
./node_modules/.bin/gh-release
|
||||||
|
|||||||
@@ -6,6 +6,5 @@ npm run update-authors
|
|||||||
git diff --exit-code
|
git diff --exit-code
|
||||||
rm -rf node_modules/
|
rm -rf node_modules/
|
||||||
npm install
|
npm install
|
||||||
npm prune
|
|
||||||
npm dedupe
|
npm dedupe
|
||||||
npm test
|
npm test
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ while (<>) {
|
|||||||
next if /<support\@greenkeeper.io>/;
|
next if /<support\@greenkeeper.io>/;
|
||||||
next if /<ungoldman\@gmail.com>/;
|
next if /<ungoldman\@gmail.com>/;
|
||||||
next if /<grunjol\@users.noreply.github.com>/;
|
next if /<grunjol\@users.noreply.github.com>/;
|
||||||
|
next if /<dc\@DCs-MacBook.local>/;
|
||||||
|
next if /<rolandoguedes\@gmail.com>/;
|
||||||
$seen{$_} = push @authors, "- ", $_;
|
$seen{$_} = push @authors, "- ", $_;
|
||||||
}
|
}
|
||||||
END {
|
END {
|
||||||
|
|||||||
23
config.js
23
config.js
@@ -1,14 +1,16 @@
|
|||||||
var appConfig = require('application-config')('WebTorrent')
|
var appConfig = require('application-config')('WebTorrent')
|
||||||
|
var fs = require('fs')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var pathExists = require('path-exists')
|
|
||||||
|
|
||||||
var APP_NAME = 'WebTorrent'
|
var APP_NAME = 'WebTorrent'
|
||||||
var APP_TEAM = 'The WebTorrent Project'
|
var APP_TEAM = 'WebTorrent, LLC'
|
||||||
var APP_VERSION = require('./package.json').version
|
var APP_VERSION = require('./package.json').version
|
||||||
|
|
||||||
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||||
|
|
||||||
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
||||||
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
||||||
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
|
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
|
||||||
@@ -17,9 +19,7 @@ module.exports = {
|
|||||||
APP_VERSION: APP_VERSION,
|
APP_VERSION: APP_VERSION,
|
||||||
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
||||||
|
|
||||||
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
|
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
|
||||||
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' +
|
|
||||||
'?version=' + APP_VERSION + '&platform=' + process.platform,
|
|
||||||
|
|
||||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||||
|
|
||||||
@@ -27,9 +27,14 @@ module.exports = {
|
|||||||
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||||
|
|
||||||
|
DELAYED_INIT: 3000 /* 3 seconds */,
|
||||||
|
|
||||||
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
||||||
|
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
||||||
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
||||||
|
|
||||||
|
HOME_PAGE_URL: 'https://webtorrent.io',
|
||||||
|
|
||||||
IS_PORTABLE: isPortable(),
|
IS_PORTABLE: isPortable(),
|
||||||
IS_PRODUCTION: isProduction(),
|
IS_PRODUCTION: isProduction(),
|
||||||
|
|
||||||
@@ -53,7 +58,11 @@ function getConfigPath () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPortable () {
|
function isPortable () {
|
||||||
return process.platform === 'win32' && isProduction() && pathExists(PORTABLE_PATH)
|
try {
|
||||||
|
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProduction () {
|
function isProduction () {
|
||||||
@@ -61,7 +70,7 @@ function isProduction () {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
return !/\/Electron\.app\/Contents\/MacOS\/Electron$/.test(process.execPath)
|
return !/\/Electron\.app\//.test(process.execPath)
|
||||||
}
|
}
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
return !/\\electron\.exe$/.test(process.execPath)
|
return !/\\electron\.exe$/.test(process.execPath)
|
||||||
|
|||||||
@@ -11,5 +11,4 @@ function init () {
|
|||||||
productName: config.APP_NAME,
|
productName: config.APP_NAME,
|
||||||
submitURL: config.CRASH_REPORT_URL
|
submitURL: config.CRASH_REPORT_URL
|
||||||
})
|
})
|
||||||
console.log('crash reporter started')
|
|
||||||
}
|
}
|
||||||
|
|||||||
38
main/announcement.js
Normal file
38
main/announcement.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
module.exports = {
|
||||||
|
init
|
||||||
|
}
|
||||||
|
|
||||||
|
var electron = require('electron')
|
||||||
|
var get = require('simple-get')
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
var log = require('./log')
|
||||||
|
|
||||||
|
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
||||||
|
'?version=' + config.APP_VERSION +
|
||||||
|
'&platform=' + process.platform
|
||||||
|
|
||||||
|
function init () {
|
||||||
|
get.concat(ANNOUNCEMENT_URL, function (err, res, data) {
|
||||||
|
if (err) return log('failed to retrieve remote message')
|
||||||
|
if (res.statusCode !== 200) return log('no remote message')
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data.toString())
|
||||||
|
} catch (err) {
|
||||||
|
data = {
|
||||||
|
title: 'WebTorrent Desktop Announcement',
|
||||||
|
message: 'WebTorrent Desktop Announcement',
|
||||||
|
detail: data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
buttons: ['OK'],
|
||||||
|
title: data.title,
|
||||||
|
message: data.message,
|
||||||
|
detail: data.detail
|
||||||
|
}, function () {})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
init
|
|
||||||
}
|
|
||||||
|
|
||||||
var electron = require('electron')
|
|
||||||
var get = require('simple-get')
|
|
||||||
|
|
||||||
var config = require('../config')
|
|
||||||
var log = require('./log')
|
|
||||||
var windows = require('./windows')
|
|
||||||
|
|
||||||
var autoUpdater = electron.autoUpdater
|
|
||||||
|
|
||||||
function init () {
|
|
||||||
autoUpdater.on('error', function (err) {
|
|
||||||
log.error('App update error: ' + err.message || err)
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.setFeedURL(config.AUTO_UPDATE_URL)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* We always check for updates on app startup. To keep app startup fast, we delay this
|
|
||||||
* first check so it happens when there is less going on.
|
|
||||||
*/
|
|
||||||
setTimeout(checkForUpdates, config.AUTO_UPDATE_CHECK_STARTUP_DELAY)
|
|
||||||
|
|
||||||
autoUpdater.on('checking-for-update', () => log('Checking for app update'))
|
|
||||||
autoUpdater.on('update-available', () => log('App update available'))
|
|
||||||
autoUpdater.on('update-not-available', () => log('App update not available'))
|
|
||||||
autoUpdater.on('update-downloaded', function (e, releaseNotes, releaseName, releaseDate, updateURL) {
|
|
||||||
log('App update downloaded: ', releaseName, updateURL)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkForUpdates () {
|
|
||||||
// Electron's built-in auto updater only supports Mac and Windows, for now
|
|
||||||
if (process.platform !== 'linux') {
|
|
||||||
return autoUpdater.checkForUpdates()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're on Linux, we have to do it ourselves
|
|
||||||
get.concat(config.AUTO_UPDATE_URL, function (err, res, data) {
|
|
||||||
if (err) return log('Error checking for app update: ' + err.message)
|
|
||||||
if (![200, 204].includes(res.statusCode)) return log('Error checking for app update, got HTTP ' + res.statusCode)
|
|
||||||
if (res.statusCode !== 200) return
|
|
||||||
|
|
||||||
var obj = JSON.parse(data)
|
|
||||||
windows.main.send('dispatch', 'updateAvailable', obj.version)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@ module.exports = {
|
|||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
|
||||||
function install () {
|
function install () {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
installDarwin()
|
installDarwin()
|
||||||
@@ -42,6 +44,12 @@ function installDarwin () {
|
|||||||
|
|
||||||
function uninstallDarwin () {}
|
function uninstallDarwin () {}
|
||||||
|
|
||||||
|
var EXEC_COMMAND = [ process.execPath ]
|
||||||
|
|
||||||
|
if (!config.IS_PRODUCTION) {
|
||||||
|
EXEC_COMMAND.push(config.ROOT_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
function installWin32 () {
|
function installWin32 () {
|
||||||
var Registry = require('winreg')
|
var Registry = require('winreg')
|
||||||
|
|
||||||
@@ -49,8 +57,8 @@ function installWin32 () {
|
|||||||
|
|
||||||
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
||||||
|
|
||||||
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, process.execPath)
|
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
|
||||||
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, process.execPath)
|
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To add a protocol handler, the following keys must be added to the Windows registry:
|
* To add a protocol handler, the following keys must be added to the Windows registry:
|
||||||
@@ -108,7 +116,7 @@ function installWin32 () {
|
|||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
||||||
})
|
})
|
||||||
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done)
|
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
|
||||||
}
|
}
|
||||||
|
|
||||||
function done (err) {
|
function done (err) {
|
||||||
@@ -169,7 +177,7 @@ function installWin32 () {
|
|||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
|
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
|
||||||
})
|
})
|
||||||
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done)
|
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
|
||||||
}
|
}
|
||||||
|
|
||||||
function done (err) {
|
function done (err) {
|
||||||
@@ -181,8 +189,8 @@ function installWin32 () {
|
|||||||
function uninstallWin32 () {
|
function uninstallWin32 () {
|
||||||
var Registry = require('winreg')
|
var Registry = require('winreg')
|
||||||
|
|
||||||
unregisterProtocolHandlerWin32('magnet', process.execPath)
|
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
||||||
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', process.execPath)
|
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
|
||||||
|
|
||||||
function unregisterProtocolHandlerWin32 (protocol, command) {
|
function unregisterProtocolHandlerWin32 (protocol, command) {
|
||||||
getCommand()
|
getCommand()
|
||||||
@@ -193,7 +201,7 @@ function uninstallWin32 () {
|
|||||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
||||||
})
|
})
|
||||||
commandKey.get('', function (err, item) {
|
commandKey.get('', function (err, item) {
|
||||||
if (!err && item.value.indexOf(command) >= 0) {
|
if (!err && item.value.indexOf(commandToArgs(command)) >= 0) {
|
||||||
destroyProtocol()
|
destroyProtocol()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -241,6 +249,10 @@ function uninstallWin32 () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commandToArgs (command) {
|
||||||
|
return command.map((arg) => `"${arg}"`).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
function installLinux () {
|
function installLinux () {
|
||||||
var fs = require('fs-extra')
|
var fs = require('fs-extra')
|
||||||
var os = require('os')
|
var os = require('os')
|
||||||
@@ -260,14 +272,14 @@ function installLinux () {
|
|||||||
function writeDesktopFile (err, desktopFile) {
|
function writeDesktopFile (err, desktopFile) {
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var appPath = config.IS_PRODUCTION ? path.dirname(process.execPath) : config.ROOT_PATH
|
var appPath = config.IS_PRODUCTION
|
||||||
var execPath = process.execPath + (config.IS_PRODUCTION ? '' : ' \.')
|
? path.dirname(process.execPath)
|
||||||
var tryExecPath = process.execPath
|
: config.ROOT_PATH
|
||||||
|
|
||||||
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
|
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
|
||||||
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
|
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
|
||||||
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, execPath)
|
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
|
||||||
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, tryExecPath)
|
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
|
||||||
|
|
||||||
var desktopFilePath = path.join(
|
var desktopFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
console.time('init')
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
var ipcMain = electron.ipcMain
|
var ipcMain = electron.ipcMain
|
||||||
|
|
||||||
var autoUpdater = require('./auto-updater')
|
var announcement = require('./announcement')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var crashReporter = require('../crash-reporter')
|
var crashReporter = require('../crash-reporter')
|
||||||
var handlers = require('./handlers')
|
var handlers = require('./handlers')
|
||||||
@@ -12,8 +14,9 @@ var log = require('./log')
|
|||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var shortcuts = require('./shortcuts')
|
var shortcuts = require('./shortcuts')
|
||||||
var squirrelWin32 = require('./squirrel-win32')
|
var squirrelWin32 = require('./squirrel-win32')
|
||||||
var windows = require('./windows')
|
|
||||||
var tray = require('./tray')
|
var tray = require('./tray')
|
||||||
|
var updater = require('./updater')
|
||||||
|
var windows = require('./windows')
|
||||||
|
|
||||||
var shouldQuit = false
|
var shouldQuit = false
|
||||||
var argv = sliceArgv(process.argv)
|
var argv = sliceArgv(process.argv)
|
||||||
@@ -23,14 +26,14 @@ if (process.platform === 'win32') {
|
|||||||
argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1)
|
argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldQuit) {
|
// if (!shouldQuit) {
|
||||||
// Prevent multiple instances of app from running at same time. New instances signal
|
// // Prevent multiple instances of app from running at same time. New instances signal
|
||||||
// this instance and quit.
|
// // this instance and quit.
|
||||||
shouldQuit = app.makeSingleInstance(onAppOpen)
|
// shouldQuit = app.makeSingleInstance(onAppOpen)
|
||||||
if (shouldQuit) {
|
// if (shouldQuit) {
|
||||||
app.quit()
|
// app.quit()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!shouldQuit) {
|
if (!shouldQuit) {
|
||||||
init()
|
init()
|
||||||
@@ -41,6 +44,7 @@ function init () {
|
|||||||
app.setPath('userData', config.CONFIG_PATH)
|
app.setPath('userData', config.CONFIG_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var 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
|
||||||
|
|
||||||
@@ -52,21 +56,24 @@ function init () {
|
|||||||
|
|
||||||
app.on('will-finish-launching', function () {
|
app.on('will-finish-launching', function () {
|
||||||
crashReporter.init()
|
crashReporter.init()
|
||||||
autoUpdater.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('ready', function () {
|
app.on('ready', function () {
|
||||||
menu.init()
|
isReady = true
|
||||||
|
|
||||||
windows.createMainWindow()
|
windows.createMainWindow()
|
||||||
windows.createWebTorrentHiddenWindow()
|
windows.createWebTorrentHiddenWindow()
|
||||||
|
menu.init()
|
||||||
shortcuts.init()
|
shortcuts.init()
|
||||||
tray.init()
|
|
||||||
handlers.install()
|
// To keep app startup fast, some code is delayed.
|
||||||
|
setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('ipcReady', function () {
|
app.on('ipcReady', function () {
|
||||||
log('Command line args:', argv)
|
log('Command line args:', argv)
|
||||||
processArgv(argv)
|
processArgv(argv)
|
||||||
|
console.timeEnd('init')
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('before-quit', function (e) {
|
app.on('before-quit', function (e) {
|
||||||
@@ -80,10 +87,17 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
windows.createMainWindow()
|
if (isReady) windows.createMainWindow()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delayedInit () {
|
||||||
|
announcement.init()
|
||||||
|
tray.init()
|
||||||
|
handlers.install()
|
||||||
|
updater.init()
|
||||||
|
}
|
||||||
|
|
||||||
function onOpen (e, torrentId) {
|
function onOpen (e, torrentId) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -120,11 +134,11 @@ function sliceArgv (argv) {
|
|||||||
function processArgv (argv) {
|
function processArgv (argv) {
|
||||||
argv.forEach(function (arg) {
|
argv.forEach(function (arg) {
|
||||||
if (arg === '-n') {
|
if (arg === '-n') {
|
||||||
windows.main.send('dispatch', 'showOpenSeedFiles')
|
menu.showOpenSeedFiles()
|
||||||
} else if (arg === '-o') {
|
} else if (arg === '-o') {
|
||||||
windows.main.send('dispatch', 'showOpenTorrentFile')
|
menu.showOpenTorrentFile()
|
||||||
} else if (arg === '-u') {
|
} else if (arg === '-u') {
|
||||||
windows.main.send('showOpenTorrentAddress')
|
menu.showOpenTorrentAddress()
|
||||||
} else if (arg.startsWith('-psn')) {
|
} else if (arg.startsWith('-psn')) {
|
||||||
// Ignore OS X launchd "process serial number" argument
|
// Ignore OS X launchd "process serial number" argument
|
||||||
// More: https://github.com/feross/webtorrent-desktop/issues/214
|
// More: https://github.com/feross/webtorrent-desktop/issues/214
|
||||||
|
|||||||
105
main/ipc.js
105
main/ipc.js
@@ -6,25 +6,29 @@ var electron = require('electron')
|
|||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
var ipcMain = electron.ipcMain
|
var ipcMain = electron.ipcMain
|
||||||
var powerSaveBlocker = electron.powerSaveBlocker
|
|
||||||
|
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
var shortcuts = require('./shortcuts')
|
var shortcuts = require('./shortcuts')
|
||||||
|
var vlc = require('./vlc')
|
||||||
|
|
||||||
// has to be a number, not a boolean, and undefined throws an error
|
// has to be a number, not a boolean, and undefined throws an error
|
||||||
var powerSaveBlockID = 0
|
var powerSaveBlockerId = 0
|
||||||
|
|
||||||
|
// messages from the main process, to be sent once the WebTorrent process starts
|
||||||
|
var messageQueueMainToWebTorrent = []
|
||||||
|
|
||||||
|
// holds a ChildProcess while we're playing a video in VLC, null otherwise
|
||||||
|
var vlcProcess
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
ipcMain.on('ipcReady', function (e) {
|
ipcMain.on('ipcReady', function (e) {
|
||||||
|
windows.main.show()
|
||||||
app.ipcReady = true
|
app.ipcReady = true
|
||||||
app.emit('ipcReady')
|
app.emit('ipcReady')
|
||||||
windows.main.show()
|
|
||||||
console.timeEnd('init')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var messageQueueMainToWebTorrent = []
|
|
||||||
ipcMain.on('ipcReadyWebTorrent', function (e) {
|
ipcMain.on('ipcReadyWebTorrent', function (e) {
|
||||||
app.ipcReadyWebTorrent = true
|
app.ipcReadyWebTorrent = true
|
||||||
log('sending %d queued messages from the main win to the webtorrent window',
|
log('sending %d queued messages from the main win to the webtorrent window',
|
||||||
@@ -36,14 +40,13 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
|
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
|
||||||
ipcMain.on('showOpenSeedFiles', menu.showOpenSeedFiles)
|
|
||||||
|
|
||||||
ipcMain.on('setBounds', function (e, bounds, maximize) {
|
ipcMain.on('setBounds', function (e, bounds, maximize) {
|
||||||
setBounds(bounds, maximize)
|
setBounds(bounds, maximize)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('setAspectRatio', function (e, aspectRatio, extraSize) {
|
ipcMain.on('setAspectRatio', function (e, aspectRatio) {
|
||||||
setAspectRatio(aspectRatio, extraSize)
|
setAspectRatio(aspectRatio)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('setBadge', function (e, text) {
|
ipcMain.on('setBadge', function (e, text) {
|
||||||
@@ -63,26 +66,83 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('openItem', function (e, path) {
|
ipcMain.on('openItem', function (e, path) {
|
||||||
log('opening file or folder: ' + path)
|
log('open item: ' + path)
|
||||||
electron.shell.openItem(path)
|
electron.shell.openItem(path)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.on('showItemInFolder', function (e, path) {
|
||||||
|
log('show item in folder: ' + path)
|
||||||
|
electron.shell.showItemInFolder(path)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.on('blockPowerSave', blockPowerSave)
|
ipcMain.on('blockPowerSave', blockPowerSave)
|
||||||
|
|
||||||
ipcMain.on('unblockPowerSave', unblockPowerSave)
|
ipcMain.on('unblockPowerSave', unblockPowerSave)
|
||||||
|
|
||||||
ipcMain.on('onPlayerOpen', function () {
|
ipcMain.on('onPlayerOpen', function () {
|
||||||
menu.onPlayerOpen()
|
menu.onPlayerOpen()
|
||||||
shortcuts.registerPlayerShortcuts()
|
shortcuts.onPlayerOpen()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('onPlayerClose', function () {
|
ipcMain.on('onPlayerClose', function () {
|
||||||
menu.onPlayerClose()
|
menu.onPlayerClose()
|
||||||
shortcuts.unregisterPlayerShortcuts()
|
shortcuts.onPlayerOpen()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('focusWindow', function (e, windowName) {
|
ipcMain.on('focusWindow', function (e, windowName) {
|
||||||
windows.focusWindow(windows[windowName])
|
windows.focusWindow(windows[windowName])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.on('downloadFinished', function (e, filePath) {
|
||||||
|
if (app.dock) {
|
||||||
|
// Bounces the Downloads stack if the filePath is inside the Downloads folder.
|
||||||
|
app.dock.downloadFinished(filePath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('checkForVLC', function (e) {
|
||||||
|
vlc.checkForVLC(function (isInstalled) {
|
||||||
|
windows.main.send('checkForVLC', isInstalled)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('vlcPlay', function (e, url) {
|
||||||
|
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url]
|
||||||
|
console.log('Running vlc ' + args.join(' '))
|
||||||
|
|
||||||
|
vlc.spawn(args, function (err, proc) {
|
||||||
|
if (err) return windows.main.send('dispatch', 'vlcNotFound')
|
||||||
|
vlcProcess = proc
|
||||||
|
|
||||||
|
// If it works, close the modal after a second
|
||||||
|
var closeModalTimeout = setTimeout(() =>
|
||||||
|
windows.main.send('dispatch', 'exitModal'), 1000)
|
||||||
|
|
||||||
|
vlcProcess.on('close', function (code) {
|
||||||
|
clearTimeout(closeModalTimeout)
|
||||||
|
if (!vlcProcess) return // Killed
|
||||||
|
console.log('VLC exited with code ', code)
|
||||||
|
if (code === 0) {
|
||||||
|
windows.main.send('dispatch', 'backToList')
|
||||||
|
} else {
|
||||||
|
windows.main.send('dispatch', 'vlcNotFound')
|
||||||
|
}
|
||||||
|
vlcProcess = null
|
||||||
|
})
|
||||||
|
|
||||||
|
vlcProcess.on('error', function (e) {
|
||||||
|
console.log('VLC error', e)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('vlcQuit', function () {
|
||||||
|
if (!vlcProcess) return
|
||||||
|
console.log('Killing VLC, pid ' + vlcProcess.pid)
|
||||||
|
vlcProcess.kill('SIGKILL') // kill -9
|
||||||
|
vlcProcess = null
|
||||||
|
})
|
||||||
|
|
||||||
// Capture all events
|
// Capture all events
|
||||||
var oldEmit = ipcMain.emit
|
var oldEmit = ipcMain.emit
|
||||||
ipcMain.emit = function (name, e, ...args) {
|
ipcMain.emit = function (name, e, ...args) {
|
||||||
@@ -142,8 +202,7 @@ function setBounds (bounds, maximize) {
|
|||||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
||||||
if (bounds.x === null && bounds.y === null) {
|
if (bounds.x === null && bounds.y === null) {
|
||||||
// X and Y not specified? By default, center on current screen
|
// X and Y not specified? By default, center on current screen
|
||||||
var screen = require('screen')
|
var scr = electron.screen.getDisplayMatching(windows.main.getBounds())
|
||||||
var scr = screen.getDisplayMatching(windows.main.getBounds())
|
|
||||||
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
||||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||||
@@ -154,17 +213,19 @@ function setBounds (bounds, maximize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAspectRatio (aspectRatio, extraSize) {
|
function setAspectRatio (aspectRatio) {
|
||||||
log('setAspectRatio %o %o', aspectRatio, extraSize)
|
log('setAspectRatio %o', aspectRatio)
|
||||||
if (windows.main) {
|
if (windows.main) {
|
||||||
windows.main.setAspectRatio(aspectRatio, extraSize)
|
windows.main.setAspectRatio(aspectRatio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display string in dock badging area (OS X)
|
// Display string in dock badging area (OS X)
|
||||||
function setBadge (text) {
|
function setBadge (text) {
|
||||||
log('setBadge %s', text)
|
log('setBadge %s', text)
|
||||||
if (app.dock) app.dock.setBadge(String(text))
|
if (app.dock) {
|
||||||
|
app.dock.setBadge(String(text))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
|
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
|
||||||
@@ -176,13 +237,13 @@ function setProgress (progress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function blockPowerSave () {
|
function blockPowerSave () {
|
||||||
powerSaveBlockID = powerSaveBlocker.start('prevent-display-sleep')
|
powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||||
log('blockPowerSave %d', powerSaveBlockID)
|
log('blockPowerSave %d', powerSaveBlockerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function unblockPowerSave () {
|
function unblockPowerSave () {
|
||||||
if (powerSaveBlocker.isStarted(powerSaveBlockID)) {
|
if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) {
|
||||||
powerSaveBlocker.stop(powerSaveBlockID)
|
electron.powerSaveBlocker.stop(powerSaveBlockerId)
|
||||||
log('unblockPowerSave %d', powerSaveBlockID)
|
log('unblockPowerSave %d', powerSaveBlockerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
255
main/menu.js
255
main/menu.js
@@ -1,11 +1,14 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
|
onPlayerClose,
|
||||||
|
onPlayerOpen,
|
||||||
onToggleFullScreen,
|
onToggleFullScreen,
|
||||||
onWindowHide,
|
onWindowHide,
|
||||||
onWindowShow,
|
onWindowShow,
|
||||||
onPlayerOpen,
|
|
||||||
onPlayerClose,
|
// TODO: move these out of menu.js -- they don't belong here
|
||||||
showOpenSeedFiles,
|
showOpenSeedFiles,
|
||||||
|
showOpenTorrentAddress,
|
||||||
showOpenTorrentFile,
|
showOpenTorrentFile,
|
||||||
toggleFullScreen
|
toggleFullScreen
|
||||||
}
|
}
|
||||||
@@ -18,20 +21,26 @@ var config = require('../config')
|
|||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
var appMenu, dockMenu
|
var appMenu
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
|
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
|
||||||
electron.Menu.setApplicationMenu(appMenu)
|
electron.Menu.setApplicationMenu(appMenu)
|
||||||
|
|
||||||
dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
|
if (app.dock) {
|
||||||
if (app.dock) app.dock.setMenu(dockMenu)
|
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
|
||||||
|
app.dock.setMenu(dockMenu)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullScreen (flag) {
|
function toggleFullScreen (flag) {
|
||||||
log('toggleFullScreen %s', flag)
|
log('toggleFullScreen %s', flag)
|
||||||
if (windows.main && windows.main.isVisible()) {
|
if (windows.main && windows.main.isVisible()) {
|
||||||
flag = flag != null ? flag : !windows.main.isFullScreen()
|
flag = flag != null ? flag : !windows.main.isFullScreen()
|
||||||
|
if (flag) {
|
||||||
|
// Allows the window to use the full screen in fullscreen mode (OS X).
|
||||||
|
windows.main.setAspectRatio(0)
|
||||||
|
}
|
||||||
windows.main.setFullScreen(flag)
|
windows.main.setFullScreen(flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,6 +55,25 @@ function toggleFloatOnTop (flag) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleDevTools () {
|
||||||
|
log('toggleDevTools')
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.toggleDevTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWebTorrentWindow () {
|
||||||
|
log('showWebTorrentWindow')
|
||||||
|
windows.webtorrent.show()
|
||||||
|
windows.webtorrent.webContents.openDevTools({ detach: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPause () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'playPause')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function increaseVolume () {
|
function increaseVolume () {
|
||||||
if (windows.main) {
|
if (windows.main) {
|
||||||
windows.main.send('dispatch', 'changeVolume', 0.1)
|
windows.main.send('dispatch', 'changeVolume', 0.1)
|
||||||
@@ -58,18 +86,12 @@ function decreaseVolume () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDevTools () {
|
function openSubtitles () {
|
||||||
log('toggleDevTools')
|
|
||||||
if (windows.main) {
|
if (windows.main) {
|
||||||
windows.main.toggleDevTools()
|
windows.main.send('dispatch', 'openSubtitles')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWebTorrentWindow () {
|
|
||||||
windows.webtorrent.show()
|
|
||||||
windows.webtorrent.webContents.openDevTools({ detach: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowShow () {
|
function onWindowShow () {
|
||||||
log('onWindowShow')
|
log('onWindowShow')
|
||||||
getMenuItem('Full Screen').enabled = true
|
getMenuItem('Full Screen').enabled = true
|
||||||
@@ -83,13 +105,19 @@ function onWindowHide () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerOpen () {
|
function onPlayerOpen () {
|
||||||
|
log('onPlayerOpen')
|
||||||
|
getMenuItem('Play/Pause').enabled = true
|
||||||
getMenuItem('Increase Volume').enabled = true
|
getMenuItem('Increase Volume').enabled = true
|
||||||
getMenuItem('Decrease Volume').enabled = true
|
getMenuItem('Decrease Volume').enabled = true
|
||||||
|
getMenuItem('Add Subtitles File...').enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function onPlayerClose () {
|
||||||
|
log('onPlayerClose')
|
||||||
|
getMenuItem('Play/Pause').enabled = false
|
||||||
getMenuItem('Increase Volume').enabled = false
|
getMenuItem('Increase Volume').enabled = false
|
||||||
getMenuItem('Decrease Volume').enabled = false
|
getMenuItem('Decrease Volume').enabled = false
|
||||||
|
getMenuItem('Add Subtitles File...').enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleFullScreen (isFullScreen) {
|
function onToggleFullScreen (isFullScreen) {
|
||||||
@@ -108,17 +136,29 @@ function getMenuItem (label) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompts the user for a file or folder, then makes a torrent out of the data
|
// Prompts the user for a file, then creates a torrent. Only allows a single file
|
||||||
|
// selection.
|
||||||
|
function showOpenSeedFile () {
|
||||||
|
electron.dialog.showOpenDialog({
|
||||||
|
title: 'Select a file for the torrent file.',
|
||||||
|
properties: [ 'openFile' ]
|
||||||
|
}, function (selectedPaths) {
|
||||||
|
if (!Array.isArray(selectedPaths)) return
|
||||||
|
var selectedPath = selectedPaths[0]
|
||||||
|
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompts the user for a file or directory, then creates a torrent. Only allows a single
|
||||||
|
// selection. To create a multi-file torrent, the user must select a directory.
|
||||||
function showOpenSeedFiles () {
|
function showOpenSeedFiles () {
|
||||||
// Allow only a single selection
|
|
||||||
// To create a multi-file torrent, the user must select a folder
|
|
||||||
electron.dialog.showOpenDialog({
|
electron.dialog.showOpenDialog({
|
||||||
title: 'Select a file or folder for the torrent file.',
|
title: 'Select a file or folder for the torrent file.',
|
||||||
properties: [ 'openFile', 'openDirectory' ]
|
properties: [ 'openFile', 'openDirectory' ]
|
||||||
}, function (filenames) {
|
}, function (selectedPaths) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(selectedPaths)) return
|
||||||
var fileOrFolder = filenames[0]
|
var selectedPath = selectedPaths[0]
|
||||||
windows.main.send('dispatch', 'showCreateTorrent', fileOrFolder)
|
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,10 +168,10 @@ function showOpenTorrentFile () {
|
|||||||
title: 'Select a .torrent file to open.',
|
title: 'Select a .torrent file to open.',
|
||||||
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
||||||
properties: [ 'openFile', 'multiSelections' ]
|
properties: [ 'openFile', 'multiSelections' ]
|
||||||
}, function (filenames) {
|
}, function (selectedPaths) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(selectedPaths)) return
|
||||||
filenames.forEach(function (filename) {
|
selectedPaths.forEach(function (selectedPath) {
|
||||||
windows.main.send('dispatch', 'addTorrent', filename)
|
windows.main.send('dispatch', 'addTorrent', selectedPath)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -142,44 +182,38 @@ function showOpenTorrentAddress () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAppMenuTemplate () {
|
function getAppMenuTemplate () {
|
||||||
var fileMenu = [
|
|
||||||
{
|
|
||||||
label: 'Create New Torrent...',
|
|
||||||
accelerator: 'CmdOrCtrl+N',
|
|
||||||
click: showOpenSeedFiles
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open Torrent File...',
|
|
||||||
accelerator: 'CmdOrCtrl+O',
|
|
||||||
click: showOpenTorrentFile
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open Torrent Address...',
|
|
||||||
accelerator: 'CmdOrCtrl+U',
|
|
||||||
click: showOpenTorrentAddress
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: process.platform === 'windows' ? 'Close' : 'Close Window',
|
|
||||||
accelerator: 'CmdOrCtrl+W',
|
|
||||||
role: 'close'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// File > Quit for Linux users with distros where the system tray is broken
|
|
||||||
if (process.platform === 'linux') {
|
|
||||||
fileMenu.push({
|
|
||||||
label: 'Quit',
|
|
||||||
click: () => app.quit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var template = [
|
var template = [
|
||||||
{
|
{
|
||||||
label: 'File',
|
label: 'File',
|
||||||
submenu: fileMenu
|
submenu: [
|
||||||
|
{
|
||||||
|
label: process.platform === 'darwin'
|
||||||
|
? 'Create New Torrent...'
|
||||||
|
: 'Create New Torrent from Folder...',
|
||||||
|
accelerator: 'CmdOrCtrl+N',
|
||||||
|
click: showOpenSeedFiles
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Open Torrent File...',
|
||||||
|
accelerator: 'CmdOrCtrl+O',
|
||||||
|
click: showOpenTorrentFile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Open Torrent Address...',
|
||||||
|
accelerator: 'CmdOrCtrl+U',
|
||||||
|
click: showOpenTorrentAddress
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: process.platform === 'win32'
|
||||||
|
? 'Close'
|
||||||
|
: 'Close Window',
|
||||||
|
accelerator: 'CmdOrCtrl+W',
|
||||||
|
role: 'close'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
@@ -225,21 +259,6 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Increase Volume',
|
|
||||||
accelerator: 'CmdOrCtrl+Up',
|
|
||||||
click: increaseVolume,
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Decrease Volume',
|
|
||||||
accelerator: 'CmdOrCtrl+Down',
|
|
||||||
click: decreaseVolume,
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Developer',
|
label: 'Developer',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -262,13 +281,36 @@ function getAppMenuTemplate () {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Window',
|
label: 'Playback',
|
||||||
role: 'window',
|
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Minimize',
|
label: 'Play/Pause',
|
||||||
accelerator: 'CmdOrCtrl+M',
|
accelerator: 'CmdOrCtrl+P',
|
||||||
role: 'minimize'
|
click: playPause,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Increase Volume',
|
||||||
|
accelerator: 'CmdOrCtrl+Up',
|
||||||
|
click: increaseVolume,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Decrease Volume',
|
||||||
|
accelerator: 'CmdOrCtrl+Down',
|
||||||
|
click: decreaseVolume,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add Subtitles File...',
|
||||||
|
click: openSubtitles,
|
||||||
|
enabled: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -278,7 +320,7 @@ function getAppMenuTemplate () {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Learn more about ' + config.APP_NAME,
|
label: 'Learn more about ' + config.APP_NAME,
|
||||||
click: () => electron.shell.openExternal('https://webtorrent.io')
|
click: () => electron.shell.openExternal(config.HOME_PAGE_URL)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Contribute on GitHub',
|
label: 'Contribute on GitHub',
|
||||||
@@ -289,14 +331,14 @@ function getAppMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Report an Issue...',
|
label: 'Report an Issue...',
|
||||||
click: () => electron.shell.openExternal(config.GITHUB_URL + '/issues')
|
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
// WebTorrent menu (OS X)
|
// Add WebTorrent app menu (OS X)
|
||||||
template.unshift({
|
template.unshift({
|
||||||
label: config.APP_NAME,
|
label: config.APP_NAME,
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -340,17 +382,35 @@ function getAppMenuTemplate () {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
// Window menu (OS X)
|
// Add Window menu (OS X)
|
||||||
template[4].submenu.push(
|
template.splice(5, 0, {
|
||||||
{
|
label: 'Window',
|
||||||
type: 'separator'
|
role: 'window',
|
||||||
},
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Bring All to Front',
|
label: 'Minimize',
|
||||||
role: 'front'
|
accelerator: 'CmdOrCtrl+M',
|
||||||
}
|
role: 'minimize'
|
||||||
)
|
},
|
||||||
} else {
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bring All to Front',
|
||||||
|
role: 'front'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// In Linux and Windows it is not possible to open both folders and files
|
||||||
|
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||||
|
// File menu (Windows, Linux)
|
||||||
|
template[0].submenu.unshift({
|
||||||
|
label: 'Create New Torrent from File...',
|
||||||
|
click: showOpenSeedFile
|
||||||
|
})
|
||||||
|
|
||||||
// Help menu (Windows, Linux)
|
// Help menu (Windows, Linux)
|
||||||
template[4].submenu.push(
|
template[4].submenu.push(
|
||||||
{
|
{
|
||||||
@@ -362,6 +422,15 @@ function getAppMenuTemplate () {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Add "File > Quit" menu item so Linux distros where the system tray icon is missing
|
||||||
|
// will have a way to quit the app.
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
// File menu (Linux)
|
||||||
|
template[0].submenu.push({
|
||||||
|
label: 'Quit',
|
||||||
|
click: () => app.quit()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
registerPlayerShortcuts,
|
onPlayerClose,
|
||||||
unregisterPlayerShortcuts
|
onPlayerOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
var localShortcut = require('electron-localshortcut')
|
|
||||||
|
|
||||||
var globalShortcut = electron.globalShortcut
|
|
||||||
|
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
// ⌘+Shift+F is an alternative fullscreen shortcut to the ones defined in menu.js.
|
var localShortcut = require('electron-localshortcut')
|
||||||
// Electron does not support multiple accelerators for a single menu item, so this
|
|
||||||
// is registered separately here.
|
// Alternate shortcuts. Most shortcuts are registered in menu,js, but Electron
|
||||||
|
// does not support multiple shortcuts for a single menu item.
|
||||||
localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen)
|
localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen)
|
||||||
|
localShortcut.register('Space', () => windows.main.send('dispatch', 'playPause'))
|
||||||
|
|
||||||
|
// Hidden shortcuts, i.e. not shown in the menu
|
||||||
|
localShortcut.register('Esc', () => windows.main.send('dispatch', 'escapeBack'))
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerPlayerShortcuts () {
|
function onPlayerOpen () {
|
||||||
// Special "media key" for play/pause, available on some keyboards
|
// Register special "media key" for play/pause, available on some keyboards
|
||||||
globalShortcut.register('MediaPlayPause', () => windows.main.send('dispatch', 'playPause'))
|
electron.globalShortcut.register(
|
||||||
|
'MediaPlayPause',
|
||||||
|
() => windows.main.send('dispatch', 'playPause')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function unregisterPlayerShortcuts () {
|
function onPlayerClose () {
|
||||||
globalShortcut.unregister('MediaPlayPause')
|
electron.globalShortcut.unregister('MediaPlayPause')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ var path = require('path')
|
|||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
var Menu = electron.Menu
|
|
||||||
var Tray = electron.Tray
|
|
||||||
|
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
@@ -35,7 +33,7 @@ function hasTray () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createTrayIcon () {
|
function createTrayIcon () {
|
||||||
trayIcon = new Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
|
trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
|
||||||
|
|
||||||
// On Windows, left click to open the app, right click for context menu
|
// On Windows, left click to open the app, right click for context menu
|
||||||
// On Linux, any click (right or left) opens the context menu
|
// On Linux, any click (right or left) opens the context menu
|
||||||
@@ -66,7 +64,7 @@ function updateTrayMenu () {
|
|||||||
} else {
|
} else {
|
||||||
showHideMenuItem = { label: 'Show', click: showApp }
|
showHideMenuItem = { label: 'Show', click: showApp }
|
||||||
}
|
}
|
||||||
var contextMenu = Menu.buildFromTemplate([
|
var contextMenu = electron.Menu.buildFromTemplate([
|
||||||
showHideMenuItem,
|
showHideMenuItem,
|
||||||
{ label: 'Quit', click: () => app.quit() }
|
{ label: 'Quit', click: () => app.quit() }
|
||||||
])
|
])
|
||||||
|
|||||||
76
main/updater.js
Normal file
76
main/updater.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
module.exports = {
|
||||||
|
init
|
||||||
|
}
|
||||||
|
|
||||||
|
var electron = require('electron')
|
||||||
|
var get = require('simple-get')
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
var log = require('./log')
|
||||||
|
var windows = require('./windows')
|
||||||
|
|
||||||
|
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||||
|
'?version=' + config.APP_VERSION +
|
||||||
|
'&platform=' + process.platform
|
||||||
|
|
||||||
|
function init () {
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
initLinux()
|
||||||
|
} else {
|
||||||
|
initDarwinWin32()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Electron auto-updater does not support Linux yet, so manually check for updates and
|
||||||
|
// `show the user a modal notification.
|
||||||
|
function initLinux () {
|
||||||
|
get.concat(AUTO_UPDATE_URL, onResponse)
|
||||||
|
|
||||||
|
function onResponse (err, res, data) {
|
||||||
|
if (err) return log(`Update error: ${err.message}`)
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
// Update available
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data)
|
||||||
|
} catch (err) {
|
||||||
|
return log(`Update error: Invalid JSON response: ${err.message}`)
|
||||||
|
}
|
||||||
|
windows.main.send('dispatch', 'updateAvailable', data.version)
|
||||||
|
} else if (res.statusCode === 204) {
|
||||||
|
// No update available
|
||||||
|
} else {
|
||||||
|
// Unexpected status code
|
||||||
|
log(`Update error: Unexpected status code: ${res.statusCode}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDarwinWin32 () {
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'error',
|
||||||
|
(err) => log.error(`Update error: ${err.message}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'checking-for-update',
|
||||||
|
() => log('Checking for update')
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'update-available',
|
||||||
|
() => log('Update available')
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'update-not-available',
|
||||||
|
() => log('Update not available')
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'update-downloaded',
|
||||||
|
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
|
||||||
|
electron.autoUpdater.checkForUpdates()
|
||||||
|
}
|
||||||
22
main/vlc.js
Normal file
22
main/vlc.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
checkForVLC,
|
||||||
|
spawn
|
||||||
|
}
|
||||||
|
|
||||||
|
var cp = require('child_process')
|
||||||
|
var vlcCommand = require('vlc-command')
|
||||||
|
|
||||||
|
// Finds if VLC is installed on Mac, Windows, or Linux.
|
||||||
|
// Calls back with true or false: whether VLC was detected
|
||||||
|
function checkForVLC (cb) {
|
||||||
|
vlcCommand((err) => cb(!err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawns VLC with child_process.spawn() to return a ChildProcess object
|
||||||
|
// Calls back with (err, childProcess)
|
||||||
|
function spawn (args, cb) {
|
||||||
|
vlcCommand(function (err, vlcPath) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
cb(null, cp.spawn(vlcPath, args))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ var windows = module.exports = {
|
|||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
|
var app = electron.app
|
||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var tray = require('./tray')
|
var tray = require('./tray')
|
||||||
@@ -68,7 +70,7 @@ function createWebTorrentHiddenWindow () {
|
|||||||
|
|
||||||
// Prevent killing the WebTorrent process
|
// Prevent killing the WebTorrent process
|
||||||
win.on('close', function (e) {
|
win.on('close', function (e) {
|
||||||
if (!electron.app.isQuitting) {
|
if (!app.isQuitting) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
win.hide()
|
win.hide()
|
||||||
}
|
}
|
||||||
@@ -79,6 +81,9 @@ function createWebTorrentHiddenWindow () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var HEADER_HEIGHT = 37
|
||||||
|
var TORRENT_HEIGHT = 100
|
||||||
|
|
||||||
function createMainWindow () {
|
function createMainWindow () {
|
||||||
if (windows.main) {
|
if (windows.main) {
|
||||||
return focusWindow(windows.main)
|
return focusWindow(windows.main)
|
||||||
@@ -89,14 +94,17 @@ function createMainWindow () {
|
|||||||
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
|
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
|
||||||
minWidth: config.WINDOW_MIN_WIDTH,
|
minWidth: config.WINDOW_MIN_WIDTH,
|
||||||
minHeight: config.WINDOW_MIN_HEIGHT,
|
minHeight: config.WINDOW_MIN_HEIGHT,
|
||||||
show: false, // Hide window until DOM finishes loading
|
show: false, // Hide window until renderer sends 'ipcReady' event
|
||||||
title: config.APP_WINDOW_TITLE,
|
title: config.APP_WINDOW_TITLE,
|
||||||
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
||||||
useContentSize: true, // Specify web page size without OS chrome
|
useContentSize: true, // Specify web page size without OS chrome
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 38 + (120 * 5) // header height + 4 torrents
|
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
||||||
})
|
})
|
||||||
win.loadURL(config.WINDOW_MAIN)
|
win.loadURL(config.WINDOW_MAIN)
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
win.setSheetOffset(HEADER_HEIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
win.webContents.on('dom-ready', function () {
|
win.webContents.on('dom-ready', function () {
|
||||||
menu.onToggleFullScreen()
|
menu.onToggleFullScreen()
|
||||||
@@ -110,8 +118,8 @@ function createMainWindow () {
|
|||||||
|
|
||||||
win.on('close', function (e) {
|
win.on('close', function (e) {
|
||||||
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
||||||
electron.app.quit()
|
app.quit()
|
||||||
} else if (!electron.app.isQuitting) {
|
} else if (!app.isQuitting) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
win.hide()
|
win.hide()
|
||||||
win.send('dispatch', 'backToList')
|
win.send('dispatch', 'backToList')
|
||||||
|
|||||||
46
package.json
46
package.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
||||||
"version": "0.3.3",
|
"version": "0.5.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Feross Aboukhadijeh",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@feross.org",
|
"email": "feross@feross.org",
|
||||||
"url": "http://feross.org"
|
"url": "https://webtorrent.io"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"webtorrent-desktop": "./bin/cmd.js"
|
"webtorrent-desktop": "./bin/cmd.js"
|
||||||
@@ -15,55 +15,62 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"airplay-js": "guerrerocarlos/node-airplay-js",
|
"airplay-js": "guerrerocarlos/node-airplay-js",
|
||||||
"application-config": "feross/node-application-config",
|
"application-config": "^0.2.1",
|
||||||
"bitfield": "^1.0.2",
|
"bitfield": "^1.0.2",
|
||||||
"chromecasts": "^1.8.0",
|
"chromecasts": "^1.8.0",
|
||||||
"concat-stream": "^1.5.1",
|
|
||||||
"create-torrent": "^3.24.5",
|
"create-torrent": "^3.24.5",
|
||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.0.3",
|
"dlnacasts": "^0.1.0",
|
||||||
"drag-drop": "^2.11.0",
|
"drag-drop": "^2.11.0",
|
||||||
"electron-localshortcut": "^0.6.0",
|
"electron-localshortcut": "^0.6.0",
|
||||||
"electron-prebuilt": "0.37.6",
|
"electron-prebuilt": "1.1.1",
|
||||||
"fs-extra": "^0.27.0",
|
"fs-extra": "^0.27.0",
|
||||||
"hyperx": "^2.0.2",
|
"hyperx": "^2.0.2",
|
||||||
|
"iso-639-1": "^1.2.1",
|
||||||
"languagedetect": "^1.1.1",
|
"languagedetect": "^1.1.1",
|
||||||
"main-loop": "^3.2.0",
|
"main-loop": "^3.2.0",
|
||||||
"musicmetadata": "^2.0.2",
|
"musicmetadata": "^2.0.2",
|
||||||
"network-address": "^1.1.0",
|
"network-address": "^1.1.0",
|
||||||
"prettier-bytes": "^1.0.1",
|
"prettier-bytes": "^1.0.1",
|
||||||
|
"run-parallel": "^1.1.6",
|
||||||
|
"simple-concat": "^1.0.0",
|
||||||
"simple-get": "^2.0.0",
|
"simple-get": "^2.0.0",
|
||||||
"srt-to-vtt": "^1.1.1",
|
"srt-to-vtt": "^1.1.1",
|
||||||
"upload-element": "^1.0.1",
|
|
||||||
"virtual-dom": "^2.1.1",
|
"virtual-dom": "^2.1.1",
|
||||||
"wcjs-player": "^0.5.7",
|
"vlc-command": "^1.0.1",
|
||||||
"webchimera.js": "^0.2.3",
|
|
||||||
"webtorrent": "0.x",
|
"webtorrent": "0.x",
|
||||||
"winreg": "^1.1.1"
|
"winreg": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-zip": "^1.0.0",
|
"cross-zip": "^2.0.1",
|
||||||
"electron-osx-sign": "^0.3.0",
|
"electron-osx-sign": "^0.3.0",
|
||||||
"electron-packager": "^7.0.0",
|
"electron-packager": "^7.0.0",
|
||||||
"electron-winstaller": "feross/windows-installer#build",
|
"electron-winstaller": "^2.3.0",
|
||||||
"gh-release": "^2.0.3",
|
"gh-release": "^2.0.3",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"nobin-debian-installer": "^0.0.9",
|
"mkdirp": "^0.5.1",
|
||||||
|
"nobin-debian-installer": "^0.0.10",
|
||||||
|
"open": "0.0.5",
|
||||||
"plist": "^1.2.0",
|
"plist": "^1.2.0",
|
||||||
|
"rimraf": "^2.5.2",
|
||||||
"run-series": "^1.1.4",
|
"run-series": "^1.1.4",
|
||||||
"standard": "^6.0.5"
|
"standard": "^7.0.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://webtorrent.io",
|
"homepage": "https://webtorrent.io",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"desktop",
|
"desktop",
|
||||||
"electron",
|
"electron",
|
||||||
"electron-app",
|
"electron-app",
|
||||||
|
"hybrid webtorrent client",
|
||||||
|
"mad science",
|
||||||
|
"torrent client",
|
||||||
|
"torrent",
|
||||||
"webtorrent"
|
"webtorrent"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"appdmg": "^0.3.6"
|
"appdmg": "^0.4.3"
|
||||||
},
|
},
|
||||||
"productName": "WebTorrent",
|
"productName": "WebTorrent",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -72,13 +79,10 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "node ./bin/clean.js",
|
"clean": "node ./bin/clean.js",
|
||||||
|
"open-config": "node ./bin/open-config.js",
|
||||||
"package": "node ./bin/package.js",
|
"package": "node ./bin/package.js",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"test": "standard",
|
"test": "standard && node ./bin/check-deps.js",
|
||||||
"update-authors": "./bin/update-authors.sh"
|
"update-authors": "./bin/update-authors.sh"
|
||||||
},
|
|
||||||
"cmake-js": {
|
|
||||||
"runtime": "electron",
|
|
||||||
"runtimeVersion": "0.37.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<body>
|
<body>
|
||||||
<img src="../static/WebTorrent.png">
|
<img src="../static/WebTorrent.png">
|
||||||
<h1>WebTorrent</h1>
|
<h1>WebTorrent</h1>
|
||||||
<p>Version <script>document.write(require('../package.json').version)</script></p>
|
<p>
|
||||||
|
Version <script>document.write(require('../package.json').version)</script>
|
||||||
|
(<script>document.write(require('webtorrent/package.json').version)</script>)
|
||||||
|
</p>
|
||||||
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -117,10 +117,6 @@ table {
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-collapse {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-collapse.expanded::before {
|
.expand-collapse.expanded::before {
|
||||||
content: '▲'
|
content: '▲'
|
||||||
}
|
}
|
||||||
@@ -276,7 +272,6 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal label {
|
.modal label {
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +362,6 @@ button { /* Rectangular text buttons */
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@@ -434,7 +428,7 @@ input {
|
|||||||
|
|
||||||
.torrent,
|
.torrent,
|
||||||
.torrent-placeholder {
|
.torrent-placeholder {
|
||||||
height: 120px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent:not(:last-child) {
|
.torrent:not(:last-child) {
|
||||||
@@ -447,9 +441,9 @@ input {
|
|||||||
|
|
||||||
.torrent .metadata {
|
.torrent .metadata {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 25px;
|
||||||
left: 20px;
|
left: 15px;
|
||||||
right: 20px;
|
right: 15px;
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
|
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
|
||||||
}
|
}
|
||||||
@@ -459,12 +453,15 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent .metadata span:not(:last-child)::after {
|
.torrent .metadata span:not(:last-child)::after {
|
||||||
content: ' — ';
|
content: ' • ';
|
||||||
|
opacity: 0.7;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .buttons {
|
.torrent .buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 25px;
|
top: 29px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -547,17 +544,11 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent .name {
|
.torrent .name {
|
||||||
font-size: 1.5em;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .status,
|
|
||||||
.torrent .status2 {
|
|
||||||
font-size: 1em;
|
|
||||||
line-height: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TORRENT LIST: DRAG-DROP TARGET
|
* TORRENT LIST: DRAG-DROP TARGET
|
||||||
*/
|
*/
|
||||||
@@ -601,11 +592,7 @@ body.drag .app::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details {
|
.torrent-details {
|
||||||
padding: 8em 20px 20px 20px;
|
padding: 8em 12px 20px 20px;
|
||||||
}
|
|
||||||
|
|
||||||
.torrent-details .open-folder {
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details table {
|
.torrent-details table {
|
||||||
@@ -619,8 +606,11 @@ body.drag .app::after {
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details tr:hover,
|
.torrent-details td {
|
||||||
.torrent-details .open-folder:hover {
|
vertical-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-details tr:hover {
|
||||||
background-color: rgba(200, 200, 200, 0.3);
|
background-color: rgba(200, 200, 200, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,16 +620,16 @@ body.drag .app::after {
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details td.col-icon {
|
.torrent-details td .icon {
|
||||||
width: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent-details td.col-icon .icon {
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent-details td.col-icon {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
.torrent-details td.col-name {
|
.torrent-details td.col-name {
|
||||||
width: auto;
|
width: auto;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -655,6 +645,11 @@ body.drag .app::after {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent-details td.col-select {
|
||||||
|
width: 2em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* PLAYER
|
* PLAYER
|
||||||
*/
|
*/
|
||||||
@@ -674,11 +669,9 @@ body.drag .app::after {
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .video-player {
|
.player video {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -762,9 +755,15 @@ body.drag .app::after {
|
|||||||
.player-controls .volume-icon,
|
.player-controls .volume-icon,
|
||||||
.player-controls .back {
|
.player-controls .back {
|
||||||
display: block;
|
display: block;
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fix for overflowing captions icon
|
||||||
|
* https://github.com/feross/webtorrent-desktop/issues/467
|
||||||
|
*/
|
||||||
|
max-width: 22px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .volume,
|
.player-controls .volume,
|
||||||
@@ -810,6 +809,7 @@ body.drag .app::after {
|
|||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
vertical-align: sub;
|
vertical-align: sub;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .volume-slider::-webkit-slider-thumb {
|
.player-controls .volume-slider::-webkit-slider-thumb {
|
||||||
@@ -820,6 +820,7 @@ body.drag .app::after {
|
|||||||
height: 10px;
|
height: 10px;
|
||||||
border: 1px solid #303233;
|
border: 1px solid #303233;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .volume-slider:focus {
|
.player-controls .volume-slider:focus {
|
||||||
@@ -989,3 +990,7 @@ body.drag .app::after {
|
|||||||
.error-popover .error:last-child {
|
.error-popover .error:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #c44;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
var appConfig = require('application-config')('WebTorrent')
|
var crashReporter = require('../crash-reporter')
|
||||||
var concat = require('concat-stream')
|
crashReporter.init()
|
||||||
var dragDrop = require('drag-drop')
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
|
// Electron apps have two processes: a main process (node) runs first and starts
|
||||||
|
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||||
|
// and this IPC channel receives from and sends messages to the main process
|
||||||
|
var ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
|
// Listen for messages from the main process
|
||||||
|
setupIpc()
|
||||||
|
|
||||||
|
var appConfig = require('application-config')('WebTorrent')
|
||||||
|
var concat = require('simple-concat')
|
||||||
|
var dragDrop = require('drag-drop')
|
||||||
var fs = require('fs-extra')
|
var fs = require('fs-extra')
|
||||||
|
var iso639 = require('iso-639-1')
|
||||||
var mainLoop = require('main-loop')
|
var mainLoop = require('main-loop')
|
||||||
|
var parallel = require('run-parallel')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var srtToVtt = require('srt-to-vtt')
|
|
||||||
var LanguageDetect = require('languagedetect')
|
|
||||||
|
|
||||||
var createElement = require('virtual-dom/create-element')
|
var createElement = require('virtual-dom/create-element')
|
||||||
var diff = require('virtual-dom/diff')
|
var diff = require('virtual-dom/diff')
|
||||||
@@ -16,7 +28,6 @@ var patch = require('virtual-dom/patch')
|
|||||||
|
|
||||||
var App = require('./views/app')
|
var App = require('./views/app')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var crashReporter = require('../crash-reporter')
|
|
||||||
var errors = require('./lib/errors')
|
var errors = require('./lib/errors')
|
||||||
var sound = require('./lib/sound')
|
var sound = require('./lib/sound')
|
||||||
var State = require('./state')
|
var State = require('./state')
|
||||||
@@ -26,18 +37,7 @@ var TorrentSummary = require('./lib/torrent-summary')
|
|||||||
var {setDispatch} = require('./lib/dispatcher')
|
var {setDispatch} = require('./lib/dispatcher')
|
||||||
setDispatch(dispatch)
|
setDispatch(dispatch)
|
||||||
|
|
||||||
appConfig.filePath = config.CONFIG_PATH + path.sep + 'config.json'
|
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||||
|
|
||||||
// Electron apps have two processes: a main process (node) runs first and starts
|
|
||||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
|
||||||
// and this IPC channel receives from and sends messages to the main process
|
|
||||||
var ipcRenderer = electron.ipcRenderer
|
|
||||||
var clipboard = electron.clipboard
|
|
||||||
|
|
||||||
var dialog = electron.remote.dialog
|
|
||||||
var Menu = electron.remote.Menu
|
|
||||||
var MenuItem = electron.remote.MenuItem
|
|
||||||
var remote = electron.remote
|
|
||||||
|
|
||||||
// This dependency is the slowest-loading, so we lazy load it
|
// This dependency is the slowest-loading, so we lazy load it
|
||||||
var Cast = null
|
var Cast = null
|
||||||
@@ -45,11 +45,10 @@ var Cast = null
|
|||||||
// For easy debugging in Developer Tools
|
// For easy debugging in Developer Tools
|
||||||
var state = global.state = State.getInitialState()
|
var state = global.state = State.getInitialState()
|
||||||
|
|
||||||
var vdomLoop
|
// Push the first page into the location history
|
||||||
|
state.location.go({ url: 'home' })
|
||||||
|
|
||||||
// Report crashes back to our server.
|
var vdomLoop
|
||||||
// Not global JS exceptions, not like Rollbar, handles segfaults/core dumps only
|
|
||||||
crashReporter.init()
|
|
||||||
|
|
||||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
// All state lives in state.js. `state.saved` is read from and written to a file.
|
||||||
// All other state is ephemeral. First we load state.saved then initialize the app.
|
// All other state is ephemeral. First we load state.saved then initialize the app.
|
||||||
@@ -64,14 +63,11 @@ function init () {
|
|||||||
// Clean up the freshly-loaded config file, which may be from an older version
|
// Clean up the freshly-loaded config file, which may be from an older version
|
||||||
cleanUpConfig()
|
cleanUpConfig()
|
||||||
|
|
||||||
// Push the first page into the location history
|
|
||||||
state.location.go({ url: 'home' })
|
|
||||||
|
|
||||||
// Restart everything we were torrenting last time the app ran
|
// Restart everything we were torrenting last time the app ran
|
||||||
resumeTorrents()
|
resumeTorrents()
|
||||||
|
|
||||||
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
||||||
window.setTimeout(delayedInit, 5000)
|
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
|
|
||||||
// The UI is built with virtual-dom, a minimalist library extracted from React
|
// The UI is built with virtual-dom, a minimalist library extracted from React
|
||||||
// The concepts--one way data flow, a pure function that renders state to a
|
// The concepts--one way data flow, a pure function that renders state to a
|
||||||
@@ -91,21 +87,15 @@ function init () {
|
|||||||
|
|
||||||
// OS integrations:
|
// OS integrations:
|
||||||
// ...drag and drop a torrent or video file to play or seed
|
// ...drag and drop a torrent or video file to play or seed
|
||||||
dragDrop('body', (files) => dispatch('onOpen', files))
|
dragDrop('body', onOpen)
|
||||||
|
|
||||||
// ...same thing if you paste a torrent
|
// ...same thing if you paste a torrent
|
||||||
document.addEventListener('paste', onPaste)
|
document.addEventListener('paste', onPaste)
|
||||||
|
|
||||||
// ...keyboard shortcuts
|
|
||||||
document.addEventListener('keydown', onKeyDown)
|
|
||||||
|
|
||||||
// ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
|
// ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
|
||||||
window.addEventListener('focus', onFocus)
|
window.addEventListener('focus', onFocus)
|
||||||
window.addEventListener('blur', onBlur)
|
window.addEventListener('blur', onBlur)
|
||||||
|
|
||||||
// Listen for messages from the main process
|
|
||||||
setupIpc()
|
|
||||||
|
|
||||||
// Done! Ideally we want to get here <100ms after the user clicks the app
|
// Done! Ideally we want to get here <100ms after the user clicks the app
|
||||||
sound.play('STARTUP')
|
sound.play('STARTUP')
|
||||||
|
|
||||||
@@ -126,10 +116,19 @@ function cleanUpConfig () {
|
|||||||
// Migration: replace torrentPath with torrentFileName
|
// Migration: replace torrentPath with torrentFileName
|
||||||
var src, dst
|
var src, dst
|
||||||
if (ts.torrentPath) {
|
if (ts.torrentPath) {
|
||||||
|
// There are a number of cases to handle here:
|
||||||
|
// * Originally we used absolute paths
|
||||||
|
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
||||||
|
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
||||||
|
// * Finally, now we're getting rid of torrentPath altogether
|
||||||
console.log('migration: replacing torrentPath %s', ts.torrentPath)
|
console.log('migration: replacing torrentPath %s', ts.torrentPath)
|
||||||
src = path.isAbsolute(ts.torrentPath)
|
if (path.isAbsolute(ts.torrentPath)) {
|
||||||
? ts.torrentPath
|
src = ts.torrentPath
|
||||||
: path.join(config.STATIC_PATH, ts.torrentPath)
|
} else if (ts.torrentPath.startsWith('..')) {
|
||||||
|
src = ts.torrentPath
|
||||||
|
} else {
|
||||||
|
src = path.join(config.STATIC_PATH, ts.torrentPath)
|
||||||
|
}
|
||||||
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
||||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
// that only runs once
|
// that only runs once
|
||||||
@@ -154,6 +153,11 @@ function cleanUpConfig () {
|
|||||||
delete ts.posterURL
|
delete ts.posterURL
|
||||||
ts.posterFileName = infoHash + extension
|
ts.posterFileName = infoHash + extension
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: add per-file selections
|
||||||
|
if (!ts.selections) {
|
||||||
|
ts.selections = ts.files.map((x) => true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,9 +215,6 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'addTorrent') {
|
if (action === 'addTorrent') {
|
||||||
addTorrent(args[0] /* torrent */)
|
addTorrent(args[0] /* torrent */)
|
||||||
}
|
}
|
||||||
if (action === 'showOpenSeedFiles') {
|
|
||||||
ipcRenderer.send('showOpenSeedFiles') /* open file or folder to seed */
|
|
||||||
}
|
|
||||||
if (action === 'showOpenTorrentFile') {
|
if (action === 'showOpenTorrentFile') {
|
||||||
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
|
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
|
||||||
}
|
}
|
||||||
@@ -226,9 +227,6 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'openFile') {
|
if (action === 'openFile') {
|
||||||
openFile(args[0] /* infoHash */, args[1] /* index */)
|
openFile(args[0] /* infoHash */, args[1] /* index */)
|
||||||
}
|
}
|
||||||
if (action === 'openFolder') {
|
|
||||||
openFolder(args[0] /* infoHash */)
|
|
||||||
}
|
|
||||||
if (action === 'toggleTorrent') {
|
if (action === 'toggleTorrent') {
|
||||||
toggleTorrent(args[0] /* infoHash */)
|
toggleTorrent(args[0] /* infoHash */)
|
||||||
}
|
}
|
||||||
@@ -238,6 +236,9 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'toggleSelectTorrent') {
|
if (action === 'toggleSelectTorrent') {
|
||||||
toggleSelectTorrent(args[0] /* infoHash */)
|
toggleSelectTorrent(args[0] /* infoHash */)
|
||||||
}
|
}
|
||||||
|
if (action === 'toggleTorrentFile') {
|
||||||
|
toggleTorrentFile(args[0] /* infoHash */, args[1] /* index */)
|
||||||
|
}
|
||||||
if (action === 'openTorrentContextMenu') {
|
if (action === 'openTorrentContextMenu') {
|
||||||
openTorrentContextMenu(args[0] /* infoHash */)
|
openTorrentContextMenu(args[0] /* infoHash */)
|
||||||
}
|
}
|
||||||
@@ -251,14 +252,16 @@ function dispatch (action, ...args) {
|
|||||||
setDimensions(args[0] /* dimensions */)
|
setDimensions(args[0] /* dimensions */)
|
||||||
}
|
}
|
||||||
if (action === 'backToList') {
|
if (action === 'backToList') {
|
||||||
while (state.location.hasBack()) state.location.back()
|
backToList()
|
||||||
|
}
|
||||||
// Work around virtual-dom issue: it doesn't expose its redraw function,
|
if (action === 'escapeBack') {
|
||||||
// and only redraws on requestAnimationFrame(). That means when the user
|
if (state.modal) {
|
||||||
// closes the window (hide window / minimize to tray) and we want to pause
|
dispatch('exitModal')
|
||||||
// the video, we update the vdom but it keeps playing until you reopen!
|
} else if (state.window.isFullScreen) {
|
||||||
var mediaTag = document.querySelector('video,audio')
|
dispatch('toggleFullScreen')
|
||||||
if (mediaTag) mediaTag.pause()
|
} else {
|
||||||
|
dispatch('back')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (action === 'back') {
|
if (action === 'back') {
|
||||||
state.location.back()
|
state.location.back()
|
||||||
@@ -270,15 +273,14 @@ function dispatch (action, ...args) {
|
|||||||
playPause()
|
playPause()
|
||||||
}
|
}
|
||||||
if (action === 'play') {
|
if (action === 'play') {
|
||||||
if (state.location.pending()) return
|
|
||||||
state.location.go({
|
state.location.go({
|
||||||
url: 'player',
|
url: 'player',
|
||||||
onbeforeload: function (cb) {
|
onbeforeload: function (cb) {
|
||||||
|
play()
|
||||||
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
|
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
|
||||||
},
|
},
|
||||||
onbeforeunload: closePlayer
|
onbeforeunload: closePlayer
|
||||||
})
|
})
|
||||||
play()
|
|
||||||
}
|
}
|
||||||
if (action === 'playbackJump') {
|
if (action === 'playbackJump') {
|
||||||
jumpToTime(args[0] /* seconds */)
|
jumpToTime(args[0] /* seconds */)
|
||||||
@@ -293,29 +295,46 @@ function dispatch (action, ...args) {
|
|||||||
openSubtitles()
|
openSubtitles()
|
||||||
}
|
}
|
||||||
if (action === 'selectSubtitle') {
|
if (action === 'selectSubtitle') {
|
||||||
selectSubtitle(args[0] /* label */)
|
selectSubtitle(args[0] /* index */)
|
||||||
}
|
}
|
||||||
if (action === 'showSubtitles') {
|
if (action === 'toggleSubtitlesMenu') {
|
||||||
showSubtitles()
|
toggleSubtitlesMenu()
|
||||||
}
|
}
|
||||||
if (action === 'mediaStalled') {
|
if (action === 'mediaStalled') {
|
||||||
state.playing.isStalled = true
|
state.playing.isStalled = true
|
||||||
}
|
}
|
||||||
if (action === 'mediaError') {
|
if (action === 'mediaError') {
|
||||||
state.location.back(function () {
|
if (state.location.url() === 'player') {
|
||||||
onError(new Error('Unsupported file format'))
|
state.playing.location = 'error'
|
||||||
})
|
ipcRenderer.send('checkForVLC')
|
||||||
|
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
||||||
|
state.modal = {
|
||||||
|
id: 'unsupported-media-modal',
|
||||||
|
error: args[0],
|
||||||
|
vlcInstalled: isInstalled
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (action === 'mediaTimeUpdate') {
|
if (action === 'mediaTimeUpdate') {
|
||||||
state.playing.lastTimeUpdate = new Date().getTime()
|
state.playing.lastTimeUpdate = new Date().getTime()
|
||||||
state.playing.isStalled = false
|
state.playing.isStalled = false
|
||||||
}
|
}
|
||||||
if (action === 'toggleFullScreen') {
|
|
||||||
ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
|
|
||||||
}
|
|
||||||
if (action === 'mediaMouseMoved') {
|
if (action === 'mediaMouseMoved') {
|
||||||
state.playing.mouseStationarySince = new Date().getTime()
|
state.playing.mouseStationarySince = new Date().getTime()
|
||||||
}
|
}
|
||||||
|
if (action === 'vlcPlay') {
|
||||||
|
ipcRenderer.send('vlcPlay', state.server.localURL)
|
||||||
|
state.playing.location = 'vlc'
|
||||||
|
}
|
||||||
|
if (action === 'vlcNotFound') {
|
||||||
|
if (state.modal && state.modal.id === 'unsupported-media-modal') {
|
||||||
|
state.modal.vlcNotFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action === 'toggleFullScreen') {
|
||||||
|
ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
|
||||||
|
}
|
||||||
if (action === 'exitModal') {
|
if (action === 'exitModal') {
|
||||||
state.modal = null
|
state.modal = null
|
||||||
}
|
}
|
||||||
@@ -365,6 +384,7 @@ function pause () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function playPause () {
|
function playPause () {
|
||||||
|
if (state.location.url() !== 'player') return
|
||||||
if (state.playing.isPaused) {
|
if (state.playing.isPaused) {
|
||||||
play()
|
play()
|
||||||
} else {
|
} else {
|
||||||
@@ -396,13 +416,31 @@ function setVolume (volume) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openSubtitles () {
|
function openSubtitles () {
|
||||||
dialog.showOpenDialog({
|
electron.remote.dialog.showOpenDialog({
|
||||||
title: 'Select a subtitles file.',
|
title: 'Select a subtitles file.',
|
||||||
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
||||||
properties: [ 'openFile' ]
|
properties: [ 'openFile' ]
|
||||||
}, function (filenames) {
|
}, function (filenames) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(filenames)) return
|
||||||
addSubtitle({path: filenames[0]})
|
addSubtitles(filenames, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quits any modal popovers and returns to the torrent list screen
|
||||||
|
function backToList () {
|
||||||
|
// Exit any modals and screens with a back button
|
||||||
|
state.modal = null
|
||||||
|
state.location.backToFirst(function () {
|
||||||
|
// If we were already on the torrent list, scroll to the top
|
||||||
|
var contentTag = document.querySelector('.content')
|
||||||
|
if (contentTag) contentTag.scrollTop = 0
|
||||||
|
|
||||||
|
// Work around virtual-dom issue: it doesn't expose its redraw function,
|
||||||
|
// and only redraws on requestAnimationFrame(). That means when the user
|
||||||
|
// closes the window (hide window / minimize to tray) and we want to pause
|
||||||
|
// the video, we update the vdom but it keeps playing until you reopen!
|
||||||
|
var mediaTag = document.querySelector('video,audio')
|
||||||
|
if (mediaTag) mediaTag.pause()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +468,10 @@ function setupIpc () {
|
|||||||
|
|
||||||
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
|
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
|
||||||
state.window.isFullScreen = isFullScreen
|
state.window.isFullScreen = isFullScreen
|
||||||
|
if (!isFullScreen) {
|
||||||
|
// Aspect ratio gets reset in fullscreen mode, so restore it (OS X)
|
||||||
|
ipcRenderer.send('setAspectRatio', state.playing.aspectRatio)
|
||||||
|
}
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -512,18 +554,32 @@ function saveState () {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called when the user drag-drops files onto the app
|
||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
if (!Array.isArray(files)) files = [ files ]
|
if (!Array.isArray(files)) files = [ files ]
|
||||||
|
|
||||||
// .torrent file = start downloading the torrent
|
if (state.modal) {
|
||||||
files.filter(isTorrent).forEach(addTorrent)
|
state.modal = null
|
||||||
|
}
|
||||||
|
|
||||||
// subtitle file
|
var subtitles = files.filter(isSubtitle)
|
||||||
files.filter(isSubtitle).forEach(addSubtitle)
|
|
||||||
|
|
||||||
// everything else = seed these files
|
if (state.location.url() === 'home' || subtitles.length === 0) {
|
||||||
var rest = files.filter(not(isTorrent)).filter(not(isSubtitle))
|
if (files.every(isTorrent)) {
|
||||||
if (rest.length > 0) showCreateTorrent(rest)
|
if (state.location.url() !== 'home') {
|
||||||
|
backToList()
|
||||||
|
}
|
||||||
|
// All .torrent files? Add them.
|
||||||
|
files.forEach(addTorrent)
|
||||||
|
} else {
|
||||||
|
// Show the Create Torrent screen. Let's seed those files.
|
||||||
|
showCreateTorrent(files)
|
||||||
|
}
|
||||||
|
} else if (state.location.url() === 'player') {
|
||||||
|
addSubtitles(subtitles, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTorrent (file) {
|
function isTorrent (file) {
|
||||||
@@ -539,12 +595,6 @@ function isSubtitle (file) {
|
|||||||
return ext === '.srt' || ext === '.vtt'
|
return ext === '.srt' || ext === '.vtt'
|
||||||
}
|
}
|
||||||
|
|
||||||
function not (test) {
|
|
||||||
return function (...args) {
|
|
||||||
return !test(...args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
|
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
|
||||||
// Returns undefined if we don't know that infoHash
|
// Returns undefined if we don't know that infoHash
|
||||||
function getTorrentSummary (torrentKey) {
|
function getTorrentSummary (torrentKey) {
|
||||||
@@ -556,6 +606,7 @@ function getTorrentSummary (torrentKey) {
|
|||||||
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
||||||
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||||
function addTorrent (torrentId) {
|
function addTorrent (torrentId) {
|
||||||
|
backToList()
|
||||||
var torrentKey = state.nextTorrentKey++
|
var torrentKey = state.nextTorrentKey++
|
||||||
var path = state.saved.downloadPath
|
var path = state.saved.downloadPath
|
||||||
if (torrentId.path) {
|
if (torrentId.path) {
|
||||||
@@ -565,46 +616,108 @@ function addTorrent (torrentId) {
|
|||||||
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSubtitle (file) {
|
function addSubtitles (files, autoSelect) {
|
||||||
|
// Subtitles are only supported while playing video
|
||||||
if (state.playing.type !== 'video') return
|
if (state.playing.type !== 'video') return
|
||||||
fs.createReadStream(file.path || file).pipe(srtToVtt()).pipe(concat(function (buf) {
|
|
||||||
|
// Read the files concurrently, then add all resulting subtitle tracks
|
||||||
|
var jobs = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||||
|
parallel(jobs, function (err, tracks) {
|
||||||
|
if (err) return onError(err)
|
||||||
|
|
||||||
|
for (var i = 0; i < tracks.length; i++) {
|
||||||
|
// No dupes allowed
|
||||||
|
var track = tracks[i]
|
||||||
|
if (state.playing.subtitles.tracks.some(
|
||||||
|
(t) => track.filePath === t.filePath)) continue
|
||||||
|
|
||||||
|
// Add the track
|
||||||
|
state.playing.subtitles.tracks.push(track)
|
||||||
|
|
||||||
|
// If we're auto-selecting a track, try to find one in the user's language
|
||||||
|
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||||
|
state.playing.subtitles.selectedIndex =
|
||||||
|
state.playing.subtitles.tracks.length - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, make sure no two tracks have the same label
|
||||||
|
relabelSubtitles()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSubtitle (file, cb) {
|
||||||
|
var srtToVtt = require('srt-to-vtt')
|
||||||
|
var LanguageDetect = require('languagedetect')
|
||||||
|
|
||||||
|
// Read the .SRT or .VTT file, parse it, add subtitle track
|
||||||
|
var filePath = file.path || file
|
||||||
|
|
||||||
|
var vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
||||||
|
|
||||||
|
concat(vttStream, function (err, buf) {
|
||||||
|
if (err) return onError(new Error('Error parsing subtitles file.'))
|
||||||
|
|
||||||
|
// Detect what language the subtitles are in
|
||||||
|
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
||||||
|
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
||||||
|
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
||||||
|
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
||||||
|
|
||||||
// Set the cue text position so it appears above the player controls.
|
// Set the cue text position so it appears above the player controls.
|
||||||
// The only way to change cue text position is by modifying the VTT. It is not
|
// The only way to change cue text position is by modifying the VTT. It is not
|
||||||
// possible via CSS.
|
// possible via CSS.
|
||||||
var langDetected = (new LanguageDetect()).detect(buf.toString().replace(/(.*-->.*)/g, ''), 2)
|
|
||||||
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
|
||||||
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
|
||||||
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
|
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
|
||||||
var track = {
|
var track = {
|
||||||
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
|
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
|
||||||
|
language: langDetected,
|
||||||
label: langDetected,
|
label: langDetected,
|
||||||
selected: true
|
filePath: filePath
|
||||||
}
|
}
|
||||||
state.playing.subtitles.tracks.forEach(function (trackItem) {
|
|
||||||
trackItem.selected = false
|
|
||||||
if (trackItem.label === track.label) {
|
|
||||||
track.label = Number.isNaN(track.label.slice(-1))
|
|
||||||
? track.label + ' 2'
|
|
||||||
: track.label.slice(0, -1) + (parseInt(track.label.slice(-1)) + 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
state.playing.subtitles.change = track.label
|
|
||||||
state.playing.subtitles.tracks.push(track)
|
|
||||||
state.playing.subtitles.enabled = true
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectSubtitle (label) {
|
cb(null, track)
|
||||||
state.playing.subtitles.tracks.forEach(function (track) {
|
|
||||||
track.selected = (track.label === label)
|
|
||||||
})
|
})
|
||||||
state.playing.subtitles.enabled = !!label
|
|
||||||
state.playing.subtitles.change = label
|
|
||||||
state.playing.subtitles.show = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSubtitles () {
|
function selectSubtitle (ix) {
|
||||||
state.playing.subtitles.show = !state.playing.subtitles.show
|
state.playing.subtitles.selectedIndex = ix
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether a language name like "English" or "German" matches the system
|
||||||
|
// language, aka the current locale
|
||||||
|
function isSystemLanguage (language) {
|
||||||
|
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
|
||||||
|
var langIso = iso639.getCode(language) // eg "de" if language is "German"
|
||||||
|
return langIso === osLangISO
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we don't have two subtitle tracks with the same label
|
||||||
|
// Labels each track by language, eg "German", "English", "English 2", ...
|
||||||
|
function relabelSubtitles () {
|
||||||
|
var counts = {}
|
||||||
|
state.playing.subtitles.tracks.forEach(function (track) {
|
||||||
|
var lang = track.language
|
||||||
|
counts[lang] = (counts[lang] || 0) + 1
|
||||||
|
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForSubtitles () {
|
||||||
|
if (state.playing.type !== 'video') return
|
||||||
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
|
if (!torrentSummary || !torrentSummary.progress) return
|
||||||
|
|
||||||
|
torrentSummary.progress.files.forEach(function (fp, ix) {
|
||||||
|
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
|
||||||
|
var file = torrentSummary.files[ix]
|
||||||
|
if (!isSubtitle(file.name)) return
|
||||||
|
var filePath = path.join(torrentSummary.path, file.path)
|
||||||
|
addSubtitles([filePath], false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSubtitlesMenu () {
|
||||||
|
state.playing.subtitles.showMenu = !state.playing.subtitles.showMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
|
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
|
||||||
@@ -624,7 +737,8 @@ function startTorrentingSummary (torrentSummary) {
|
|||||||
torrentID = s.magnetURI || s.infoHash
|
torrentID = s.magnetURI || s.infoHash
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes)
|
console.log('start torrenting %s %s', s.torrentKey, torrentID)
|
||||||
|
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes, s.selections)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -635,7 +749,6 @@ function startTorrentingSummary (torrentSummary) {
|
|||||||
// Shows the Create Torrent page with options to seed a given file or folder
|
// Shows the Create Torrent page with options to seed a given file or folder
|
||||||
function showCreateTorrent (files) {
|
function showCreateTorrent (files) {
|
||||||
if (Array.isArray(files)) {
|
if (Array.isArray(files)) {
|
||||||
if (state.location.pending() || state.location.current().url !== 'home') return
|
|
||||||
state.location.go({
|
state.location.go({
|
||||||
url: 'create-torrent',
|
url: 'create-torrent',
|
||||||
files: files
|
files: files
|
||||||
@@ -685,6 +798,9 @@ function findFilesRecursive (fileOrFolder, cb) {
|
|||||||
function createTorrent (options) {
|
function createTorrent (options) {
|
||||||
var torrentKey = state.nextTorrentKey++
|
var torrentKey = state.nextTorrentKey++
|
||||||
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||||
|
state.location.backToFirst(function () {
|
||||||
|
state.location.clearForward('create-torrent')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentInfoHash (torrentKey, infoHash) {
|
function torrentInfoHash (torrentKey, infoHash) {
|
||||||
@@ -697,7 +813,7 @@ function torrentInfoHash (torrentKey, infoHash) {
|
|||||||
torrentKey: torrentKey,
|
torrentKey: torrentKey,
|
||||||
status: 'new'
|
status: 'new'
|
||||||
}
|
}
|
||||||
state.saved.torrents.push(torrentSummary)
|
state.saved.torrents.unshift(torrentSummary)
|
||||||
sound.play('ADD')
|
sound.play('ADD')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,16 +826,16 @@ function torrentWarning (torrentKey, message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function torrentError (torrentKey, message) {
|
function torrentError (torrentKey, message) {
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
// TODO: WebTorrent needs semantic errors
|
||||||
|
if (message.startsWith('Cannot add duplicate torrent')) {
|
||||||
|
// Remove infohash from the message
|
||||||
|
message = 'Cannot add duplicate torrent'
|
||||||
|
}
|
||||||
|
onError(message)
|
||||||
|
|
||||||
// TODO: WebTorrent should have semantic errors
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
if (message.startsWith('There is already a swarm')) {
|
if (torrentSummary) {
|
||||||
onError(new Error('Can\'t add duplicate torrent'))
|
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
|
||||||
} else if (!torrentSummary) {
|
|
||||||
onError(message)
|
|
||||||
} else {
|
|
||||||
console.log('error, stopping torrent %s (%s):\n\t%o',
|
|
||||||
torrentSummary.name, torrentSummary.infoHash, message)
|
|
||||||
torrentSummary.status = 'paused'
|
torrentSummary.status = 'paused'
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
@@ -733,6 +849,9 @@ function torrentMetadata (torrentKey, torrentInfo) {
|
|||||||
torrentSummary.path = torrentInfo.path
|
torrentSummary.path = torrentInfo.path
|
||||||
torrentSummary.files = torrentInfo.files
|
torrentSummary.files = torrentInfo.files
|
||||||
torrentSummary.magnetURI = torrentInfo.magnetURI
|
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||||
|
if (!torrentSummary.selections) {
|
||||||
|
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||||
|
}
|
||||||
update()
|
update()
|
||||||
|
|
||||||
// Save the .torrent file, if it hasn't been saved already
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
@@ -754,6 +873,7 @@ function torrentDone (torrentKey, torrentInfo) {
|
|||||||
state.dock.badge += 1
|
state.dock.badge += 1
|
||||||
}
|
}
|
||||||
showDoneNotification(torrentSummary)
|
showDoneNotification(torrentSummary)
|
||||||
|
ipcRenderer.send('downloadFinished', getTorrentPath(torrentSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
update()
|
update()
|
||||||
@@ -783,6 +903,8 @@ function torrentProgress (progressInfo) {
|
|||||||
torrentSummary.progress = p
|
torrentSummary.progress = p
|
||||||
})
|
})
|
||||||
|
|
||||||
|
checkForSubtitles()
|
||||||
|
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,6 +1004,9 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
|||||||
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if it's video, check for subtitles files that are done downloading
|
||||||
|
checkForSubtitles()
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
||||||
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, function (e, info) {
|
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, function (e, info) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
@@ -908,6 +1033,9 @@ function closePlayer (cb) {
|
|||||||
if (isCasting()) {
|
if (isCasting()) {
|
||||||
Cast.close()
|
Cast.close()
|
||||||
}
|
}
|
||||||
|
if (state.playing.location === 'vlc') {
|
||||||
|
ipcRenderer.send('vlcQuit')
|
||||||
|
}
|
||||||
state.window.title = config.APP_WINDOW_TITLE
|
state.window.title = config.APP_WINDOW_TITLE
|
||||||
state.playing = State.getDefaultPlayState()
|
state.playing = State.getDefaultPlayState()
|
||||||
state.server = null
|
state.server = null
|
||||||
@@ -933,17 +1061,6 @@ function openFile (infoHash, index) {
|
|||||||
ipcRenderer.send('openItem', filePath)
|
ipcRenderer.send('openItem', filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFolder (infoHash) {
|
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
|
||||||
|
|
||||||
var firstFilePath = path.join(
|
|
||||||
torrentSummary.path,
|
|
||||||
torrentSummary.files[0].path)
|
|
||||||
var folderPath = path.dirname(firstFilePath)
|
|
||||||
|
|
||||||
ipcRenderer.send('openItem', folderPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
function toggleTorrent (infoHash) {
|
function toggleTorrent (infoHash) {
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
@@ -965,7 +1082,7 @@ function deleteTorrent (infoHash) {
|
|||||||
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||||
if (index > -1) state.saved.torrents.splice(index, 1)
|
if (index > -1) state.saved.torrents.splice(index, 1)
|
||||||
saveStateThrottled()
|
saveStateThrottled()
|
||||||
state.location.clearForward() // prevent user from going forward to a deleted torrent
|
state.location.clearForward('player') // prevent user from going forward to a deleted torrent
|
||||||
sound.play('DELETE')
|
sound.play('DELETE')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,25 +1092,56 @@ function toggleSelectTorrent (infoHash) {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleTorrentFile (infoHash, index) {
|
||||||
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
|
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
||||||
|
|
||||||
|
// Let the WebTorrent process know to start or stop fetching that file
|
||||||
|
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||||
|
}
|
||||||
|
|
||||||
function openTorrentContextMenu (infoHash) {
|
function openTorrentContextMenu (infoHash) {
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
var menu = new Menu()
|
var menu = new electron.remote.Menu()
|
||||||
menu.append(new MenuItem({
|
|
||||||
|
if (torrentSummary.files) {
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
|
||||||
|
click: () => showItemInFolder(torrentSummary)
|
||||||
|
}))
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
type: 'separator'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
label: 'Copy Magnet Link to Clipboard',
|
||||||
|
click: () => electron.clipboard.writeText(torrentSummary.magnetURI)
|
||||||
|
}))
|
||||||
|
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
|
label: 'Copy Instant.io Link to Clipboard',
|
||||||
|
click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
|
||||||
|
}))
|
||||||
|
|
||||||
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Save Torrent File As...',
|
label: 'Save Torrent File As...',
|
||||||
click: () => saveTorrentFileAs(torrentSummary)
|
click: () => saveTorrentFileAs(torrentSummary)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new MenuItem({
|
menu.popup(electron.remote.getCurrentWindow())
|
||||||
label: 'Copy Instant.io Link to Clipboard',
|
}
|
||||||
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
|
|
||||||
}))
|
|
||||||
|
|
||||||
menu.append(new MenuItem({
|
function getTorrentPath (torrentSummary) {
|
||||||
label: 'Copy Magnet Link to Clipboard',
|
var itemPath = path.join(torrentSummary.path, torrentSummary.files[0].path)
|
||||||
click: () => clipboard.writeText(torrentSummary.magnetURI)
|
if (torrentSummary.files.length > 1) {
|
||||||
}))
|
itemPath = path.dirname(itemPath)
|
||||||
|
}
|
||||||
|
return itemPath
|
||||||
|
}
|
||||||
|
|
||||||
menu.popup(remote.getCurrentWindow())
|
function showItemInFolder (torrentSummary) {
|
||||||
|
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTorrentFileAs (torrentSummary) {
|
function saveTorrentFileAs (torrentSummary) {
|
||||||
@@ -1006,7 +1154,7 @@ function saveTorrentFileAs (torrentSummary) {
|
|||||||
{ name: 'All Files', extensions: ['*'] }
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
dialog.showSaveDialog(remote.getCurrentWindow(), opts, function (savePath) {
|
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
||||||
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
@@ -1020,7 +1168,7 @@ function saveTorrentFileAs (torrentSummary) {
|
|||||||
// Set window dimensions to match video dimensions or fill the screen
|
// Set window dimensions to match video dimensions or fill the screen
|
||||||
function setDimensions (dimensions) {
|
function setDimensions (dimensions) {
|
||||||
// Don't modify the window size if it's already maximized
|
// Don't modify the window size if it's already maximized
|
||||||
if (remote.getCurrentWindow().isMaximized()) {
|
if (electron.remote.getCurrentWindow().isMaximized()) {
|
||||||
state.window.bounds = null
|
state.window.bounds = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1032,7 +1180,7 @@ function setDimensions (dimensions) {
|
|||||||
width: window.outerWidth,
|
width: window.outerWidth,
|
||||||
height: window.outerHeight
|
height: window.outerHeight
|
||||||
}
|
}
|
||||||
state.window.wasMaximized = remote.getCurrentWindow().isMaximized
|
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
|
||||||
|
|
||||||
// Limit window size to screen size
|
// Limit window size to screen size
|
||||||
var screenWidth = window.screen.width
|
var screenWidth = window.screen.width
|
||||||
@@ -1053,6 +1201,7 @@ function setDimensions (dimensions) {
|
|||||||
|
|
||||||
ipcRenderer.send('setAspectRatio', aspectRatio)
|
ipcRenderer.send('setAspectRatio', aspectRatio)
|
||||||
ipcRenderer.send('setBounds', {x: null, y: null, width, height})
|
ipcRenderer.send('setBounds', {x: null, y: null, width, height})
|
||||||
|
state.playing.aspectRatio = aspectRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreBounds () {
|
function restoreBounds () {
|
||||||
@@ -1081,7 +1230,7 @@ function showDoneNotification (torrent) {
|
|||||||
// * 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
|
||||||
function showOrHidePlayerControls () {
|
function showOrHidePlayerControls () {
|
||||||
var hideControls = state.location.current().url === 'player' &&
|
var hideControls = state.location.url() === 'player' &&
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
state.playing.mouseStationarySince !== 0 &&
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||||
!state.playing.isPaused &&
|
!state.playing.isPaused &&
|
||||||
@@ -1112,26 +1261,14 @@ function onWarning (err) {
|
|||||||
function onPaste (e) {
|
function onPaste (e) {
|
||||||
if (e.target.tagName.toLowerCase() === 'input') return
|
if (e.target.tagName.toLowerCase() === 'input') return
|
||||||
|
|
||||||
var torrentIds = clipboard.readText().split('\n')
|
var torrentIds = electron.clipboard.readText().split('\n')
|
||||||
torrentIds.forEach(function (torrentId) {
|
torrentIds.forEach(function (torrentId) {
|
||||||
torrentId = torrentId.trim()
|
torrentId = torrentId.trim()
|
||||||
if (torrentId.length === 0) return
|
if (torrentId.length === 0) return
|
||||||
dispatch('addTorrent', torrentId)
|
addTorrent(torrentId)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
function onKeyDown (e) {
|
update()
|
||||||
if (e.which === 27) { /* ESC means either exit fullscreen or go back */
|
|
||||||
if (state.modal) {
|
|
||||||
dispatch('exitModal')
|
|
||||||
} else if (state.window.isFullScreen) {
|
|
||||||
dispatch('toggleFullScreen')
|
|
||||||
} else {
|
|
||||||
dispatch('back')
|
|
||||||
}
|
|
||||||
} else if (e.which === 32) { /* spacebar pauses or plays the video */
|
|
||||||
dispatch('playPause')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFocus (e) {
|
function onFocus (e) {
|
||||||
|
|||||||
@@ -21,12 +21,10 @@ function dispatcher (...args) {
|
|||||||
var handler = _dispatchers[json]
|
var handler = _dispatchers[json]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
handler = _dispatchers[json] = (e) => {
|
handler = _dispatchers[json] = (e) => {
|
||||||
if (e && e.stopPropagation && e.currentTarget) {
|
// Don't click on whatever is below the button
|
||||||
// Don't click on whatever is below the button
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
// Don't regisiter clicks on disabled buttons
|
||||||
// Don't register clicks on disabled buttons
|
if (e.currentTarget.classList.contains('disabled')) return
|
||||||
if (e.currentTarget.classList.contains('disabled')) return
|
|
||||||
}
|
|
||||||
_dispatch.apply(null, args)
|
_dispatch.apply(null, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,81 +4,123 @@ function LocationHistory () {
|
|||||||
if (!new.target) return new LocationHistory()
|
if (!new.target) return new LocationHistory()
|
||||||
this._history = []
|
this._history = []
|
||||||
this._forward = []
|
this._forward = []
|
||||||
this._pending = null
|
this._pending = false
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.go = function (page, cb) {
|
LocationHistory.prototype.url = function () {
|
||||||
console.log('go', page)
|
return this.current() && this.current().url
|
||||||
this.clearForward()
|
|
||||||
this._go(page, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype._go = function (page, cb) {
|
|
||||||
if (this._pending) return
|
|
||||||
if (page.onbeforeload) {
|
|
||||||
this._pending = page
|
|
||||||
page.onbeforeload((err) => {
|
|
||||||
if (this._pending !== page) return /* navigation was cancelled */
|
|
||||||
this._pending = null
|
|
||||||
if (err) {
|
|
||||||
if (cb) cb(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._history.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this._history.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype.back = function (cb) {
|
|
||||||
if (this._history.length <= 1) return
|
|
||||||
|
|
||||||
var page = this._history.pop()
|
|
||||||
|
|
||||||
if (page.onbeforeunload) {
|
|
||||||
// TODO: this is buggy. If the user clicks back twice, then those pages
|
|
||||||
// may end up in _forward in the wrong order depending on which onbeforeunload
|
|
||||||
// call finishes first.
|
|
||||||
page.onbeforeunload(() => {
|
|
||||||
this._forward.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this._forward.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype.forward = function (cb) {
|
|
||||||
if (this._forward.length === 0) return
|
|
||||||
|
|
||||||
var page = this._forward.pop()
|
|
||||||
this._go(page, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype.clearForward = function () {
|
|
||||||
this._forward = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.current = function () {
|
LocationHistory.prototype.current = function () {
|
||||||
return this._history[this._history.length - 1]
|
return this._history[this._history.length - 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.go = function (page, cb) {
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (this._pending) return cb(null)
|
||||||
|
|
||||||
|
console.log('go', page)
|
||||||
|
|
||||||
|
this.clearForward()
|
||||||
|
this._go(page, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.back = function (cb) {
|
||||||
|
var self = this
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (self._history.length <= 1 || self._pending) return cb(null)
|
||||||
|
|
||||||
|
var page = self._history.pop()
|
||||||
|
self._unload(page, done)
|
||||||
|
|
||||||
|
function done (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
self._forward.push(page)
|
||||||
|
self._load(self.current(), cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.hasBack = function () {
|
LocationHistory.prototype.hasBack = function () {
|
||||||
return this._history.length > 1
|
return this._history.length > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.forward = function (cb) {
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (this._forward.length === 0 || this._pending) return cb(null)
|
||||||
|
|
||||||
|
var page = this._forward.pop()
|
||||||
|
this._go(page, cb)
|
||||||
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.hasForward = function () {
|
LocationHistory.prototype.hasForward = function () {
|
||||||
return this._forward.length > 0
|
return this._forward.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.pending = function () {
|
LocationHistory.prototype.clearForward = function (url) {
|
||||||
return this._pending
|
if (url == null) {
|
||||||
|
this._forward = []
|
||||||
|
} else {
|
||||||
|
console.log(this._forward)
|
||||||
|
console.log(url)
|
||||||
|
this._forward = this._forward.filter(function (page) {
|
||||||
|
return page.url !== url
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.clearPending = function () {
|
LocationHistory.prototype.backToFirst = function (cb) {
|
||||||
this._pending = null
|
var self = this
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (self._history.length <= 1) return cb(null)
|
||||||
|
|
||||||
|
self.back(function (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
self.backToFirst(cb)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype._go = function (page, cb) {
|
||||||
|
var self = this
|
||||||
|
if (!cb) cb = noop
|
||||||
|
|
||||||
|
self._unload(self.current(), done1)
|
||||||
|
|
||||||
|
function done1 (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
self._load(page, done2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function done2 (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
self._history.push(page)
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype._load = function (page, cb) {
|
||||||
|
var self = this
|
||||||
|
self._pending = true
|
||||||
|
|
||||||
|
if (page && page.onbeforeload) page.onbeforeload(done)
|
||||||
|
else done(null)
|
||||||
|
|
||||||
|
function done (err) {
|
||||||
|
self._pending = false
|
||||||
|
cb(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype._unload = function (page, cb) {
|
||||||
|
var self = this
|
||||||
|
self._pending = true
|
||||||
|
|
||||||
|
if (page && page.onbeforeunload) page.onbeforeunload(done)
|
||||||
|
else done(null)
|
||||||
|
|
||||||
|
function done (err) {
|
||||||
|
self._pending = false
|
||||||
|
cb(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function noop () {}
|
||||||
|
|||||||
@@ -6,41 +6,43 @@ module.exports = {
|
|||||||
var config = require('../../config')
|
var config = require('../../config')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
|
var VOLUME = 0.15
|
||||||
|
|
||||||
/* Cache of Audio elements, for instant playback */
|
/* Cache of Audio elements, for instant playback */
|
||||||
var cache = {}
|
var cache = {}
|
||||||
|
|
||||||
var sounds = {
|
var sounds = {
|
||||||
ADD: {
|
ADD: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
||||||
volume: 0.1
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
DISABLE: {
|
DISABLE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
DONE: {
|
DONE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
ENABLE: {
|
ENABLE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
ERROR: {
|
ERROR: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
PLAY: {
|
PLAY: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
STARTUP: {
|
STARTUP: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
||||||
volume: 0.4
|
volume: VOLUME * 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,27 @@ function isPlayable (file) {
|
|||||||
|
|
||||||
function isVideo (file) {
|
function isVideo (file) {
|
||||||
var ext = path.extname(file.name).toLowerCase()
|
var ext = path.extname(file.name).toLowerCase()
|
||||||
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
|
return [
|
||||||
|
'.avi',
|
||||||
|
'.m4v',
|
||||||
|
'.mkv',
|
||||||
|
'.mov',
|
||||||
|
'.mp4',
|
||||||
|
'.mpg',
|
||||||
|
'.ogv',
|
||||||
|
'.webm'
|
||||||
|
].includes(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAudio (file) {
|
function isAudio (file) {
|
||||||
var ext = path.extname(file.name).toLowerCase()
|
var ext = path.extname(file.name).toLowerCase()
|
||||||
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
|
return [
|
||||||
|
'.aac',
|
||||||
|
'.ac3',
|
||||||
|
'.mp3',
|
||||||
|
'.ogg',
|
||||||
|
'.wav'
|
||||||
|
].includes(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlayableTorrent (torrentSummary) {
|
function isPlayableTorrent (torrentSummary) {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ var LocationHistory = require('./lib/location-history')
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getInitialState,
|
getInitialState,
|
||||||
getDefaultPlayState,
|
getDefaultPlayState,
|
||||||
getDefaultSavedState
|
getDefaultSavedState,
|
||||||
|
getPlayingTorrentSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialState () {
|
function getInitialState () {
|
||||||
@@ -57,7 +58,12 @@ function getInitialState () {
|
|||||||
*
|
*
|
||||||
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
||||||
*/
|
*/
|
||||||
saved: {}
|
saved: {},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Getters, for convenience
|
||||||
|
*/
|
||||||
|
getPlayingTorrentSummary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,9 +81,11 @@ function getDefaultPlayState () {
|
|||||||
lastTimeUpdate: 0, /* Unix time in ms */
|
lastTimeUpdate: 0, /* Unix time in ms */
|
||||||
mouseStationarySince: 0, /* Unix time in ms */
|
mouseStationarySince: 0, /* Unix time in ms */
|
||||||
subtitles: {
|
subtitles: {
|
||||||
tracks: [], /* subtitles file (Buffer) */
|
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
||||||
enabled: false
|
selectedIndex: -1, /* current subtitle track */
|
||||||
}
|
showMenu: false /* popover menu, above the video */
|
||||||
|
},
|
||||||
|
aspectRatio: 0 /* aspect ratio of the video */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,3 +270,8 @@ function getDefaultSavedState () {
|
|||||||
: remote.app.getPath('downloads')
|
: remote.app.getPath('downloads')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPlayingTorrentSummary () {
|
||||||
|
var infoHash = this.playing.infoHash
|
||||||
|
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ var Views = {
|
|||||||
}
|
}
|
||||||
var Modals = {
|
var Modals = {
|
||||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||||
'update-available-modal': require('./update-available-modal')
|
'update-available-modal': require('./update-available-modal'),
|
||||||
|
'unsupported-media-modal': require('./unsupported-media-modal')
|
||||||
}
|
}
|
||||||
|
|
||||||
function App (state) {
|
function App (state) {
|
||||||
@@ -21,7 +22,7 @@ function App (state) {
|
|||||||
// * 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
|
||||||
var hideControls = state.location.current().url === 'player' &&
|
var hideControls = state.location.url() === 'player' &&
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
state.playing.mouseStationarySince !== 0 &&
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||||
!state.playing.isPaused &&
|
!state.playing.isPaused &&
|
||||||
@@ -29,10 +30,10 @@ function App (state) {
|
|||||||
|
|
||||||
// Hide the header on Windows/Linux when in the player
|
// Hide the header on Windows/Linux when in the player
|
||||||
// On OSX, the header appears as part of the title bar
|
// On OSX, the header appears as part of the title bar
|
||||||
var hideHeader = process.platform !== 'darwin' && state.location.current().url === 'player'
|
var hideHeader = process.platform !== 'darwin' && state.location.url() === 'player'
|
||||||
|
|
||||||
var cls = [
|
var cls = [
|
||||||
'view-' + state.location.current().url, /* e.g. view-home, view-player */
|
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
||||||
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
||||||
]
|
]
|
||||||
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
||||||
@@ -53,12 +54,13 @@ function App (state) {
|
|||||||
function getErrorPopover (state) {
|
function getErrorPopover (state) {
|
||||||
var now = new Date().getTime()
|
var now = new Date().getTime()
|
||||||
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
||||||
|
var hasErrors = recentErrors.length > 0
|
||||||
|
|
||||||
var errorElems = recentErrors.map(function (error) {
|
var errorElems = recentErrors.map(function (error) {
|
||||||
return hx`<div class='error'>${error.message}</div>`
|
return hx`<div class='error'>${error.message}</div>`
|
||||||
})
|
})
|
||||||
return hx`
|
return hx`
|
||||||
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
|
<div class='error-popover ${hasErrors ? 'visible' : 'hidden'}'>
|
||||||
<div class='title'>Error</div>
|
<div class='title'>Error</div>
|
||||||
${errorElems}
|
${errorElems}
|
||||||
</div>
|
</div>
|
||||||
@@ -79,6 +81,6 @@ function getModal (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getView (state) {
|
function getView (state) {
|
||||||
var url = state.location.current().url
|
var url = state.location.url()
|
||||||
return Views[url](state)
|
return Views[url](state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ function CreateTorrentPage (state) {
|
|||||||
|
|
||||||
// Sanity check: show the number of files and total size
|
// Sanity check: show the number of files and total size
|
||||||
var numFiles = files.length
|
var numFiles = files.length
|
||||||
console.log('FILES', files)
|
|
||||||
var totalBytes = files
|
var totalBytes = files
|
||||||
.map((f) => f.size)
|
.map((f) => f.size)
|
||||||
.reduce((a, b) => a + b, 0)
|
.reduce((a, b) => a + b, 0)
|
||||||
@@ -41,8 +40,16 @@ function CreateTorrentPage (state) {
|
|||||||
|
|
||||||
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
||||||
// as the default name. Show all files relative to the base folder.
|
// as the default name. Show all files relative to the base folder.
|
||||||
var defaultName = path.basename(pathPrefix)
|
var defaultName, basePath
|
||||||
var basePath = path.dirname(pathPrefix)
|
if (files.length === 1) {
|
||||||
|
// Single file torrent: /a/b/foo.jpg -> torrent name "foo.jpg", path "/a/b"
|
||||||
|
defaultName = files[0].name
|
||||||
|
basePath = pathPrefix
|
||||||
|
} else {
|
||||||
|
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a"
|
||||||
|
defaultName = path.basename(pathPrefix)
|
||||||
|
basePath = path.dirname(pathPrefix)
|
||||||
|
}
|
||||||
var maxFileElems = 100
|
var maxFileElems = 100
|
||||||
var fileElems = files.slice(0, maxFileElems).map(function (file) {
|
var fileElems = files.slice(0, maxFileElems).map(function (file) {
|
||||||
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
|
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
|
||||||
@@ -112,11 +119,10 @@ function CreateTorrentPage (state) {
|
|||||||
comment: comment
|
comment: comment
|
||||||
}
|
}
|
||||||
dispatch('createTorrent', options)
|
dispatch('createTorrent', options)
|
||||||
dispatch('backToList')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel () {
|
function handleCancel () {
|
||||||
dispatch('backToList')
|
dispatch('back')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleShowAdvanced () {
|
function handleToggleShowAdvanced () {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function Header (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAddButton () {
|
function getAddButton () {
|
||||||
if (state.location.current().url !== 'player') {
|
if (state.location.url() !== 'player') {
|
||||||
return hx`
|
return hx`
|
||||||
<i
|
<i
|
||||||
class='icon add'
|
class='icon add'
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ var h = require('virtual-dom/h')
|
|||||||
var hyperx = require('hyperx')
|
var hyperx = require('hyperx')
|
||||||
var hx = hyperx(h)
|
var hx = hyperx(h)
|
||||||
|
|
||||||
var WebChimeraPlayer = require('wcjs-player')
|
|
||||||
var prettyBytes = require('prettier-bytes')
|
var prettyBytes = require('prettier-bytes')
|
||||||
var Bitfield = require('bitfield')
|
var Bitfield = require('bitfield')
|
||||||
|
|
||||||
@@ -19,6 +18,7 @@ function Player (state) {
|
|||||||
return hx`
|
return hx`
|
||||||
<div
|
<div
|
||||||
class='player'
|
class='player'
|
||||||
|
onwheel=${handleVolumeWheel}
|
||||||
onmousemove=${dispatcher('mediaMouseMoved')}>
|
onmousemove=${dispatcher('mediaMouseMoved')}>
|
||||||
${showVideo ? renderMedia(state) : renderCastScreen(state)}
|
${showVideo ? renderMedia(state) : renderCastScreen(state)}
|
||||||
${renderPlayerControls(state)}
|
${renderPlayerControls(state)}
|
||||||
@@ -26,16 +26,14 @@ function Player (state) {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMedia (state) {
|
// Handles volume change by wheel
|
||||||
if (!state.server) return
|
function handleVolumeWheel (e) {
|
||||||
if (false) return renderMediaTag(state)
|
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
|
||||||
else return renderMediaVLC(state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders using a <video> or <audio> tag
|
function renderMedia (state) {
|
||||||
// Handles only a subset of codecs, but it's cleaner and more efficient
|
if (!state.server) return
|
||||||
// See renderMediaVLC()
|
|
||||||
function renderMediaTag (state) {
|
|
||||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||||
// Instead, grab the DOM node and play/pause it if necessary
|
// Instead, grab the DOM node and play/pause it if necessary
|
||||||
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
|
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
|
||||||
@@ -56,14 +54,10 @@ function renderMediaTag (state) {
|
|||||||
state.playing.setVolume = null
|
state.playing.setVolume = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix textTrack cues not been removed <track> rerender
|
// Switch to the newly added subtitle track, if available
|
||||||
if (state.playing.subtitles.change) {
|
var tracks = mediaElement.textTracks
|
||||||
var tracks = mediaElement.textTracks
|
for (var j = 0; j < tracks.length; j++) {
|
||||||
for (var j = 0; j < tracks.length; j++) {
|
tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden'
|
||||||
// mode is not an <track> attribute, only available on DOM
|
|
||||||
tracks[j].mode = (tracks[j].label === state.playing.subtitles.change) ? 'showing' : 'hidden'
|
|
||||||
}
|
|
||||||
state.playing.subtitles.change = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.playing.currentTime = mediaElement.currentTime
|
state.playing.currentTime = mediaElement.currentTime
|
||||||
@@ -73,12 +67,13 @@ function renderMediaTag (state) {
|
|||||||
|
|
||||||
// Add subtitles to the <video> tag
|
// Add subtitles to the <video> tag
|
||||||
var trackTags = []
|
var trackTags = []
|
||||||
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
|
if (state.playing.subtitles.selectedIndex >= 0) {
|
||||||
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||||
var track = state.playing.subtitles.tracks[i]
|
var track = state.playing.subtitles.tracks[i]
|
||||||
|
var isSelected = state.playing.subtitles.selectedIndex === i
|
||||||
trackTags.push(hx`
|
trackTags.push(hx`
|
||||||
<track
|
<track
|
||||||
${track.selected ? 'default' : ''}
|
${isSelected ? 'default' : ''}
|
||||||
label=${track.label}
|
label=${track.label}
|
||||||
type='subtitles'
|
type='subtitles'
|
||||||
src=${track.buffer}>
|
src=${track.buffer}>
|
||||||
@@ -96,7 +91,8 @@ function renderMediaTag (state) {
|
|||||||
onstalling=${dispatcher('mediaStalled')}
|
onstalling=${dispatcher('mediaStalled')}
|
||||||
onerror=${dispatcher('mediaError')}
|
onerror=${dispatcher('mediaError')}
|
||||||
ontimeupdate=${dispatcher('mediaTimeUpdate')}
|
ontimeupdate=${dispatcher('mediaTimeUpdate')}
|
||||||
autoplay>
|
onencrypted=${dispatcher('mediaEncrypted')}
|
||||||
|
oncanplay=${onCanPlay}>
|
||||||
${trackTags}
|
${trackTags}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -127,101 +123,15 @@ function renderMediaTag (state) {
|
|||||||
function onEnded (e) {
|
function onEnded (e) {
|
||||||
state.playing.isPaused = true
|
state.playing.isPaused = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Renders using WebChimera.js to render using VLC
|
function onCanPlay (e) {
|
||||||
// That lets us play media that the <video> tag can't play
|
var video = e.target
|
||||||
function renderMediaVLC (state) {
|
if (video.webkitVideoDecodedByteCount > 0 &&
|
||||||
// Unfortunately, WebChimera can't be done just by modifying HTML.
|
video.webkitAudioDecodedByteCount === 0) {
|
||||||
// Instead, grab the DOM node
|
dispatch('mediaError', 'Audio codec unsupported')
|
||||||
if (document.querySelector('#media-player')) {
|
|
||||||
if (!state.playing.chimera) {
|
|
||||||
state.playing.chimera = new WebChimeraPlayer('#media-player')
|
|
||||||
.addPlayer({
|
|
||||||
autoplay: true,
|
|
||||||
vlcArgs: ['-vvv'],
|
|
||||||
wcjsRendererOptions: {'fallbackRenderer': true}
|
|
||||||
})
|
|
||||||
.onPlaying(dispatcher('mediaPlaying'))
|
|
||||||
.onPaused(dispatcher('mediaPaused'))
|
|
||||||
.onBuffering(dispatcher('mediaStalled'))
|
|
||||||
.onTime(dispatcher('mediaTimeUpdate'))
|
|
||||||
.onEnded(onEnded)
|
|
||||||
.onFrameSetup(onLoadedMetadata)
|
|
||||||
.addPlaylist(state.server.localURL)
|
|
||||||
state.playing.chimera.ui(false)
|
|
||||||
} else {
|
} else {
|
||||||
var player = state.playing.chimera
|
video.play()
|
||||||
if (state.playing.isPaused && player.playing()) {
|
|
||||||
player.pause()
|
|
||||||
} else if (!state.playing.isPaused && !player.playing()) {
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
// When the user clicks or drags on the progress bar, jump to that position
|
|
||||||
if (state.playing.jumpToTime) {
|
|
||||||
player.time(state.playing.jumpToTime * 1000) // WebChimera expects milliseconds
|
|
||||||
state.playing.jumpToTime = null
|
|
||||||
}
|
|
||||||
// Set volume
|
|
||||||
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
|
||||||
player.volume(Math.round(state.playing.setVolume * 100)) // WebChimera expects integer percent
|
|
||||||
state.playing.setVolume = null
|
|
||||||
}
|
|
||||||
|
|
||||||
state.playing.currentTime = player.time() / 1000
|
|
||||||
state.playing.duration = player.length() / 1000
|
|
||||||
state.playing.volume = player.volume() / 100
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
state.playing.chimera = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add subtitles to the <video> tag
|
|
||||||
var trackTags = []
|
|
||||||
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
|
|
||||||
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
|
||||||
var track = state.playing.subtitles.tracks[i]
|
|
||||||
trackTags.push(hx`
|
|
||||||
<track
|
|
||||||
default=${track.selected ? 'default' : ''}
|
|
||||||
label=${track.language}
|
|
||||||
type='subtitles'
|
|
||||||
src=${track.buffer}>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the <audio> or <video> tag
|
|
||||||
var mediaType = state.playing.type /* 'video' or 'audio' */
|
|
||||||
var mediaTag = hx`
|
|
||||||
<div id='media-player' class='${mediaType}-player'>
|
|
||||||
${trackTags}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
// Show the media.
|
|
||||||
return hx`
|
|
||||||
<div
|
|
||||||
class='letterbox'
|
|
||||||
onmousemove=${dispatcher('mediaMouseMoved')}>
|
|
||||||
${mediaTag}
|
|
||||||
${renderOverlay(state)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
// As soon as the video loads enough to know the video dimensions, resize the window
|
|
||||||
function onLoadedMetadata (e) {
|
|
||||||
if (mediaType !== 'video') return
|
|
||||||
var dimensions = {
|
|
||||||
width: player.width(),
|
|
||||||
height: player.height()
|
|
||||||
}
|
|
||||||
dispatch('setDimensions', dimensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the video completes, pause the video instead of looping
|
|
||||||
function onEnded (e) {
|
|
||||||
state.playing.isPaused = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +161,7 @@ function renderOverlay (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAudioMetadata (state) {
|
function renderAudioMetadata (state) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
var fileSummary = torrentSummary.files[state.playing.fileIndex]
|
var fileSummary = torrentSummary.files[state.playing.fileIndex]
|
||||||
if (!fileSummary.audioInfo) return
|
if (!fileSummary.audioInfo) return
|
||||||
var info = fileSummary.audioInfo
|
var info = fileSummary.audioInfo
|
||||||
@@ -290,7 +200,7 @@ function renderLoadingSpinner (state) {
|
|||||||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
||||||
if (!isProbablyStalled) return
|
if (!isProbablyStalled) return
|
||||||
|
|
||||||
var prog = getPlayingTorrentSummary(state).progress || {}
|
var prog = state.getPlayingTorrentSummary().progress || {}
|
||||||
var fileProgress = 0
|
var fileProgress = 0
|
||||||
if (prog.files) {
|
if (prog.files) {
|
||||||
var file = prog.files[state.playing.fileIndex]
|
var file = prog.files[state.playing.fileIndex]
|
||||||
@@ -310,20 +220,33 @@ function renderLoadingSpinner (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderCastScreen (state) {
|
function renderCastScreen (state) {
|
||||||
var castIcon, castType
|
var castIcon, castType, isCast
|
||||||
if (state.playing.location.startsWith('chromecast')) {
|
if (state.playing.location.startsWith('chromecast')) {
|
||||||
castIcon = 'cast_connected'
|
castIcon = 'cast_connected'
|
||||||
castType = 'Chromecast'
|
castType = 'Chromecast'
|
||||||
|
isCast = true
|
||||||
} else if (state.playing.location.startsWith('airplay')) {
|
} else if (state.playing.location.startsWith('airplay')) {
|
||||||
castIcon = 'airplay'
|
castIcon = 'airplay'
|
||||||
castType = 'AirPlay'
|
castType = 'AirPlay'
|
||||||
|
isCast = true
|
||||||
} else if (state.playing.location.startsWith('dlna')) {
|
} else if (state.playing.location.startsWith('dlna')) {
|
||||||
castIcon = 'tv'
|
castIcon = 'tv'
|
||||||
castType = 'DLNA'
|
castType = 'DLNA'
|
||||||
|
isCast = true
|
||||||
|
} else if (state.playing.location === 'vlc') {
|
||||||
|
castIcon = 'tv'
|
||||||
|
castType = 'VLC'
|
||||||
|
isCast = false
|
||||||
|
} else if (state.playing.location === 'error') {
|
||||||
|
castIcon = 'error_outline'
|
||||||
|
castType = 'Error'
|
||||||
|
isCast = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var isStarting = state.playing.location.endsWith('-pending')
|
var isStarting = state.playing.location.endsWith('-pending')
|
||||||
var castStatus = isStarting ? 'Connecting...' : 'Connected'
|
var castStatus
|
||||||
|
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
|
||||||
|
else castStatus = ''
|
||||||
|
|
||||||
// Show a nice title image, if possible
|
// Show a nice title image, if possible
|
||||||
var style = {
|
var style = {
|
||||||
@@ -343,15 +266,28 @@ function renderCastScreen (state) {
|
|||||||
|
|
||||||
function renderSubtitlesOptions (state) {
|
function renderSubtitlesOptions (state) {
|
||||||
var subtitles = state.playing.subtitles
|
var subtitles = state.playing.subtitles
|
||||||
if (subtitles.tracks.length && subtitles.show) {
|
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||||
return hx`<ul.subtitles-list>
|
|
||||||
${subtitles.tracks.map(function (w, i) {
|
var items = subtitles.tracks.map(function (track, ix) {
|
||||||
return hx`<li onclick=${dispatcher('selectSubtitle', w.label)}><i.icon>${w.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>${w.label}</li>`
|
var isSelected = state.playing.subtitles.selectedIndex === ix
|
||||||
})}
|
return hx`
|
||||||
<li onclick=${dispatcher('selectSubtitle', '')}><i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>None</li>
|
<li onclick=${dispatcher('selectSubtitle', ix)}>
|
||||||
</ul>
|
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||||
|
${track.label}
|
||||||
|
</li>
|
||||||
`
|
`
|
||||||
}
|
})
|
||||||
|
|
||||||
|
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||||
|
return hx`
|
||||||
|
<ul.subtitles-list>
|
||||||
|
${items}
|
||||||
|
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
||||||
|
<i.icon>${noneSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||||
|
None
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPlayerControls (state) {
|
function renderPlayerControls (state) {
|
||||||
@@ -359,7 +295,7 @@ function renderPlayerControls (state) {
|
|||||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
||||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
var captionsClass = state.playing.subtitles.tracks.length === 0
|
||||||
? 'disabled'
|
? 'disabled'
|
||||||
: state.playing.subtitles.enabled
|
: state.playing.subtitles.selectedIndex >= 0
|
||||||
? 'active'
|
? 'active'
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
@@ -476,8 +412,7 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<div.volume
|
<div.volume>
|
||||||
onwheel=${handleVolumeWheel}>
|
|
||||||
<i.icon.volume-icon onmousedown=${handleVolumeMute}>
|
<i.icon.volume-icon onmousedown=${handleVolumeMute}>
|
||||||
${volumeIcon}
|
${volumeIcon}
|
||||||
</i>
|
</i>
|
||||||
@@ -486,7 +421,6 @@ function renderPlayerControls (state) {
|
|||||||
onmousedown=${handleVolumeScrub}
|
onmousedown=${handleVolumeScrub}
|
||||||
onmouseup=${handleVolumeScrub}
|
onmouseup=${handleVolumeScrub}
|
||||||
onmousemove=${handleVolumeScrub}
|
onmousemove=${handleVolumeScrub}
|
||||||
onwheel=${handleVolumeWheel}
|
|
||||||
style=${volumeStyle}
|
style=${volumeStyle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -515,11 +449,6 @@ function renderPlayerControls (state) {
|
|||||||
dispatch('playbackJump', position)
|
dispatch('playbackJump', position)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles volume change by wheel
|
|
||||||
function handleVolumeWheel (e) {
|
|
||||||
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles volume muting and Unmuting
|
// Handles volume muting and Unmuting
|
||||||
function handleVolumeMute (e) {
|
function handleVolumeMute (e) {
|
||||||
if (state.playing.volume === 0.0) {
|
if (state.playing.volume === 0.0) {
|
||||||
@@ -553,7 +482,7 @@ function renderPlayerControls (state) {
|
|||||||
// if no subtitles available select it
|
// if no subtitles available select it
|
||||||
dispatch('openSubtitles')
|
dispatch('openSubtitles')
|
||||||
} else {
|
} else {
|
||||||
dispatch('showSubtitles')
|
dispatch('toggleSubtitlesMenu')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,7 +493,7 @@ var volumeChanging = false
|
|||||||
// 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) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary.progress) {
|
if (!torrentSummary.progress) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -601,7 +530,7 @@ function renderLoadingBar (state) {
|
|||||||
|
|
||||||
// Returns the CSS background-image string for a poster image + dark vignette
|
// Returns the CSS background-image string for a poster image + dark vignette
|
||||||
function cssBackgroundImagePoster (state) {
|
function cssBackgroundImagePoster (state) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
if (!posterPath) return ''
|
if (!posterPath) return ''
|
||||||
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
||||||
@@ -611,8 +540,3 @@ function cssBackgroundImageDarkGradient () {
|
|||||||
return 'radial-gradient(circle at center, ' +
|
return 'radial-gradient(circle at center, ' +
|
||||||
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayingTorrentSummary (state) {
|
|
||||||
var infoHash = state.playing.infoHash
|
|
||||||
return state.saved.torrents.find((x) => x.infoHash === infoHash)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -67,39 +67,49 @@ function TorrentList (state) {
|
|||||||
// If it's downloading/seeding then show progress info
|
// If it's downloading/seeding then show progress info
|
||||||
var prog = torrentSummary.progress
|
var prog = torrentSummary.progress
|
||||||
if (torrentSummary.status !== 'paused' && prog) {
|
if (torrentSummary.status !== 'paused' && prog) {
|
||||||
var progress = Math.floor(100 * prog.progress)
|
|
||||||
var downloaded = prettyBytes(prog.downloaded)
|
|
||||||
var total = prettyBytes(prog.length || 0)
|
|
||||||
if (downloaded !== total) downloaded += ` / ${total}`
|
|
||||||
|
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<div class='status ellipsis'>
|
<div class='ellipsis'>
|
||||||
${getFilesLength()}
|
${renderPercentProgress()}
|
||||||
<span>${getPeers()}</span>
|
${renderTotalProgress()}
|
||||||
<span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
|
${renderPeers()}
|
||||||
<span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
|
${renderDownloadSpeed()}
|
||||||
</div>
|
${renderUploadSpeed()}
|
||||||
`)
|
|
||||||
elements.push(hx`
|
|
||||||
<div class='status2 ellipsis'>
|
|
||||||
<span class='progress'>${progress}%</span>
|
|
||||||
<span>${downloaded}</span>
|
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return hx`<div class='metadata'>${elements}</div>`
|
return hx`<div class='metadata'>${elements}</div>`
|
||||||
|
|
||||||
function getPeers () {
|
function renderPercentProgress () {
|
||||||
var count = prog.numPeers === 1 ? 'peer' : 'peers'
|
var progress = Math.floor(100 * prog.progress)
|
||||||
return `${prog.numPeers} ${count}`
|
return hx`<span>${progress}%</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilesLength () {
|
function renderTotalProgress () {
|
||||||
if (torrentSummary.files && torrentSummary.files.length > 1) {
|
var downloaded = prettyBytes(prog.downloaded)
|
||||||
return hx`<span class='files'>${torrentSummary.files.length} files</span>`
|
var total = prettyBytes(prog.length || 0)
|
||||||
|
if (downloaded === total) {
|
||||||
|
return hx`<span>${downloaded}</span>`
|
||||||
|
} else {
|
||||||
|
return hx`<span>${downloaded} / ${total}</span>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPeers () {
|
||||||
|
if (prog.numPeers === 0) return
|
||||||
|
var count = prog.numPeers === 1 ? 'peer' : 'peers'
|
||||||
|
return hx`<span>${prog.numPeers} ${count}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDownloadSpeed () {
|
||||||
|
if (prog.downloadSpeed === 0) return
|
||||||
|
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUploadSpeed () {
|
||||||
|
if (prog.uploadSpeed === 0) return
|
||||||
|
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download button toggles between torrenting (DL/seed) and paused
|
// Download button toggles between torrenting (DL/seed) and paused
|
||||||
@@ -167,7 +177,6 @@ function TorrentList (state) {
|
|||||||
|
|
||||||
// Show files, per-file download status and play buttons, and so on
|
// Show files, per-file download status and play buttons, and so on
|
||||||
function renderTorrentDetails (torrentSummary) {
|
function renderTorrentDetails (torrentSummary) {
|
||||||
var infoHash = torrentSummary.infoHash
|
|
||||||
var filesElement
|
var filesElement
|
||||||
if (!torrentSummary.files) {
|
if (!torrentSummary.files) {
|
||||||
// We don't know what files this torrent contains
|
// We don't know what files this torrent contains
|
||||||
@@ -182,10 +191,6 @@ function TorrentList (state) {
|
|||||||
filesElement = hx`
|
filesElement = hx`
|
||||||
<div class='files'>
|
<div class='files'>
|
||||||
<strong>Files</strong>
|
<strong>Files</strong>
|
||||||
<span class='open-folder'
|
|
||||||
onclick=${dispatcher('openFolder', infoHash)}>
|
|
||||||
Open folder
|
|
||||||
</span>
|
|
||||||
<table>
|
<table>
|
||||||
${fileRows}
|
${fileRows}
|
||||||
</table>
|
</table>
|
||||||
@@ -203,7 +208,8 @@ function TorrentList (state) {
|
|||||||
// Show a single torrentSummary file in the details view for a single torrent
|
// Show a single torrentSummary file in the details view for a single torrent
|
||||||
function renderFileRow (torrentSummary, file, index) {
|
function renderFileRow (torrentSummary, file, index) {
|
||||||
// First, find out how much of the file we've downloaded
|
// First, find out how much of the file we've downloaded
|
||||||
var isDone = false
|
var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
|
||||||
|
var isDone = false // Are we finished torrenting it?
|
||||||
var progress = ''
|
var progress = ''
|
||||||
if (torrentSummary.progress && torrentSummary.progress.files) {
|
if (torrentSummary.progress && torrentSummary.progress.files) {
|
||||||
var fileProg = torrentSummary.progress.files[index]
|
var fileProg = torrentSummary.progress.files[index]
|
||||||
@@ -212,26 +218,38 @@ function TorrentList (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Second, render the file as a table row
|
// Second, render the file as a table row
|
||||||
|
var isPlayable = TorrentPlayer.isPlayable(file)
|
||||||
var infoHash = torrentSummary.infoHash
|
var infoHash = torrentSummary.infoHash
|
||||||
var icon
|
var icon
|
||||||
var rowClass = ''
|
|
||||||
var handleClick
|
var handleClick
|
||||||
if (TorrentPlayer.isPlayable(file)) {
|
if (isPlayable) {
|
||||||
icon = 'play_arrow' /* playable? add option to play */
|
icon = 'play_arrow' /* playable? add option to play */
|
||||||
handleClick = dispatcher('play', infoHash, index)
|
handleClick = dispatcher('play', infoHash, index)
|
||||||
} else {
|
} else {
|
||||||
icon = 'description' /* file icon, opens in OS default app */
|
icon = 'description' /* file icon, opens in OS default app */
|
||||||
rowClass = isDone ? '' : 'disabled'
|
|
||||||
handleClick = dispatcher('openFile', infoHash, index)
|
handleClick = dispatcher('openFile', infoHash, index)
|
||||||
}
|
}
|
||||||
|
var rowClass = ''
|
||||||
|
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||||
|
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||||
return hx`
|
return hx`
|
||||||
<tr onclick=${handleClick} class='${rowClass}'>
|
<tr>
|
||||||
<td class='col-icon'>
|
<td class='col-icon ${rowClass}' onclick=${handleClick}>
|
||||||
<i class='icon'>${icon}</i>
|
<i class='icon'>${icon}</i>
|
||||||
</td>
|
</td>
|
||||||
<td class='col-name'>${file.name}</td>
|
<td class='col-name ${rowClass}' onclick=${handleClick}>
|
||||||
<td class='col-progress'>${progress}</td>
|
${file.name}
|
||||||
<td class='col-size'>${prettyBytes(file.length)}</td>
|
</td>
|
||||||
|
<td class='col-progress ${rowClass}' onclick=${handleClick}>
|
||||||
|
${isSelected ? progress : ''}
|
||||||
|
</td>
|
||||||
|
<td class='col-size ${rowClass}' onclick=${handleClick}>
|
||||||
|
${prettyBytes(file.length)}
|
||||||
|
</td>
|
||||||
|
<td class='col-select'
|
||||||
|
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||||
|
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
42
renderer/views/unsupported-media-modal.js
Normal file
42
renderer/views/unsupported-media-modal.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
module.exports = UnsupportedMediaModal
|
||||||
|
|
||||||
|
var h = require('virtual-dom/h')
|
||||||
|
var hyperx = require('hyperx')
|
||||||
|
var hx = hyperx(h)
|
||||||
|
|
||||||
|
var electron = require('electron')
|
||||||
|
|
||||||
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
function UnsupportedMediaModal (state) {
|
||||||
|
var err = state.modal.error
|
||||||
|
var message = (err && err.getMessage)
|
||||||
|
? err.getMessage()
|
||||||
|
: err
|
||||||
|
var actionButton = state.modal.vlcInstalled
|
||||||
|
? hx`<button class="button-raised" onclick=${onPlay}>Play in VLC</button>`
|
||||||
|
: hx`<button class="button-raised" onclick=${onInstall}>Install VLC</button>`
|
||||||
|
var vlcMessage = state.modal.vlcNotFound
|
||||||
|
? 'Couldn\'t run VLC. Please make sure it\'s installed.'
|
||||||
|
: ''
|
||||||
|
return hx`
|
||||||
|
<div>
|
||||||
|
<p><strong>Sorry, we can't play that file.</strong></p>
|
||||||
|
<p>${message}</p>
|
||||||
|
<p class='float-right'>
|
||||||
|
<button class="button-flat" onclick=${dispatcher('backToList')}>Cancel</button>
|
||||||
|
${actionButton}
|
||||||
|
</p>
|
||||||
|
<p class='error-text'>${vlcMessage}</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
function onInstall () {
|
||||||
|
electron.shell.openExternal('http://www.videolan.org/vlc/')
|
||||||
|
state.modal.vlcInstalled = true // Assume they'll install it successfully
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlay () {
|
||||||
|
dispatch('vlcPlay')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,14 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
|||||||
|
|
||||||
// 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
|
||||||
var client = window.client = new WebTorrent()
|
var client = window.client = new WebTorrent({
|
||||||
|
tracker: {
|
||||||
|
// HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome bug.
|
||||||
|
// Fixed in Chrome 51, so we can remove this hack once Electron updates Chrome.
|
||||||
|
// Issue: https://github.com/feross/webtorrent-desktop/issues/353
|
||||||
|
wrtc: process.platform !== 'darwin'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// WebTorrent-to-HTTP streaming sever
|
// WebTorrent-to-HTTP streaming sever
|
||||||
var server = window.server = null
|
var server = window.server = null
|
||||||
@@ -42,8 +49,8 @@ function init () {
|
|||||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||||
|
|
||||||
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) =>
|
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
||||||
startTorrenting(torrentKey, torrentID, path, fileModtimes))
|
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
||||||
ipc.on('wt-stop-torrenting', (e, infoHash) =>
|
ipc.on('wt-stop-torrenting', (e, infoHash) =>
|
||||||
stopTorrenting(infoHash))
|
stopTorrenting(infoHash))
|
||||||
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
|
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
|
||||||
@@ -58,6 +65,8 @@ function init () {
|
|||||||
startServer(infoHash, index))
|
startServer(infoHash, index))
|
||||||
ipc.on('wt-stop-server', (e) =>
|
ipc.on('wt-stop-server', (e) =>
|
||||||
stopServer())
|
stopServer())
|
||||||
|
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
||||||
|
selectFiles(infoHash, selections))
|
||||||
|
|
||||||
ipc.send('ipcReadyWebTorrent')
|
ipc.send('ipcReadyWebTorrent')
|
||||||
|
|
||||||
@@ -66,31 +75,27 @@ function init () {
|
|||||||
|
|
||||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
||||||
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||||
function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
|
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
||||||
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
||||||
var torrent
|
|
||||||
try {
|
var torrent = client.add(torrentID, {
|
||||||
torrent = client.add(torrentID, {
|
path: path,
|
||||||
path: path,
|
fileModtimes: fileModtimes
|
||||||
fileModtimes: fileModtimes
|
})
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
return ipc.send('wt-error', torrentKey, err.message)
|
|
||||||
}
|
|
||||||
// If we add a duplicate magnet URI or infohash, WebTorrent returns the
|
|
||||||
// existing torrent object! (If we add a duplicate torrent file, it creates a
|
|
||||||
// new torrent object and raises an error later.) Workaround:
|
|
||||||
if (torrent.key) {
|
|
||||||
return ipc.send('wt-error', torrentKey, 'Can\'t add duplicate torrent')
|
|
||||||
}
|
|
||||||
torrent.key = torrentKey
|
torrent.key = torrentKey
|
||||||
|
|
||||||
|
// Listen for ready event, progress notifications, etc
|
||||||
addTorrentEvents(torrent)
|
addTorrentEvents(torrent)
|
||||||
|
|
||||||
|
// Only download the files the user wants, not necessarily all files
|
||||||
|
torrent.once('ready', () => selectFiles(torrent, selections))
|
||||||
|
|
||||||
return torrent
|
return torrent
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopTorrenting (infoHash) {
|
function stopTorrenting (infoHash) {
|
||||||
var torrent = client.get(infoHash)
|
var torrent = client.get(infoHash)
|
||||||
torrent.destroy()
|
if (torrent) torrent.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new torrent, start seeding
|
// Create a new torrent, start seeding
|
||||||
@@ -159,9 +164,7 @@ function getTorrentFileInfo (file) {
|
|||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
length: file.length,
|
length: file.length,
|
||||||
path: file.path,
|
path: file.path
|
||||||
numPiecesPresent: 0,
|
|
||||||
numPieces: null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +315,44 @@ function getAudioMetadata (infoHash, index) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectFiles (torrentOrInfoHash, selections) {
|
||||||
|
// Get the torrent object
|
||||||
|
var torrent
|
||||||
|
if (typeof torrentOrInfoHash === 'string') {
|
||||||
|
torrent = client.get(torrentOrInfoHash)
|
||||||
|
} else {
|
||||||
|
torrent = torrentOrInfoHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selections not specified?
|
||||||
|
// Load all files. We still need to replace the default whole-torrent
|
||||||
|
// selection with individual selections for each file, so we can
|
||||||
|
// select/deselect files later on
|
||||||
|
if (!selections) {
|
||||||
|
selections = torrent.files.map((x) => true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selections specified incorrectly?
|
||||||
|
if (selections.length !== torrent.files.length) {
|
||||||
|
throw new Error('got ' + selections.length + ' file selections, ' +
|
||||||
|
'but the torrent contains ' + torrent.files.length + ' files')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove default selection (whole torrent)
|
||||||
|
torrent.deselect(0, torrent.pieces.length - 1, false)
|
||||||
|
|
||||||
|
// Add selections (individual files)
|
||||||
|
for (var i = 0; i < selections.length; i++) {
|
||||||
|
var file = torrent.files[i]
|
||||||
|
if (selections[i]) {
|
||||||
|
file.select()
|
||||||
|
} else {
|
||||||
|
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
|
||||||
|
file.deselect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Gets a WebTorrent handle by torrentKey
|
// Gets a WebTorrent handle by torrentKey
|
||||||
// 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) {
|
||||||
|
|||||||
10
static/child.entitlements
Normal file
10
static/child.entitlements
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.inherit</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
20
static/parent.entitlements
Normal file
20
static/parent.entitlements
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>5MAMC8G3L8.io.webtorrent.webtorrent</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Reference in New Issue
Block a user