Compare commits
153 Commits
entitlemen
...
v0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e34223fc94 | ||
|
|
15f733f11c | ||
|
|
7526b18507 | ||
|
|
0af6007632 | ||
|
|
1bc3cd1d51 | ||
|
|
92bafd695d | ||
|
|
78a2ee4e85 | ||
|
|
8b9346d767 | ||
|
|
06d3bd3f93 | ||
|
|
1af7e4ef19 | ||
|
|
8e64e4120b | ||
|
|
b983559763 | ||
|
|
e62527de23 | ||
|
|
1f51f35f8e | ||
|
|
c3686417e3 | ||
|
|
746e10c025 | ||
|
|
98389fc07c | ||
|
|
aaebf93db4 | ||
|
|
12500dfb64 | ||
|
|
acc8e7923a | ||
|
|
9aa5775528 | ||
|
|
2a2d71289a | ||
|
|
ae28e34fd5 | ||
|
|
6b175e7d40 | ||
|
|
2c6d74e8ef | ||
|
|
3b832595fe | ||
|
|
bf372029fb | ||
|
|
17ce7e519c | ||
|
|
1f6a112df7 | ||
|
|
9d3e26f15a | ||
|
|
8a95895254 | ||
|
|
5d71f9e9c6 | ||
|
|
0ec6fb5a93 | ||
|
|
5d410457ce | ||
|
|
c6cd21b8ff | ||
|
|
2235b2fa82 | ||
|
|
65e0b5d6e7 | ||
|
|
ea64411570 | ||
|
|
9348c61a84 | ||
|
|
d9aa3822ee | ||
|
|
e86bd26800 | ||
|
|
6d8cec17de | ||
|
|
572f084570 | ||
|
|
4a3ca5459d | ||
|
|
e8cb6abf0a | ||
|
|
94b3bc561d | ||
|
|
5eb75d0250 | ||
|
|
b577e08053 | ||
|
|
dae4840bd6 | ||
|
|
57eb52a606 | ||
|
|
6d670bdd3f | ||
|
|
def2209dc5 | ||
|
|
763c573c7a | ||
|
|
eb61f2ac0e | ||
|
|
a9d1925686 | ||
|
|
0e10eba073 | ||
|
|
0427e1f3a6 | ||
|
|
c841c94784 | ||
|
|
e3c6049fdb | ||
|
|
829206e921 | ||
|
|
f7acdffb2a | ||
|
|
cc9ba385bf | ||
|
|
e88ddd648b | ||
|
|
dac34541d6 | ||
|
|
52fb378fd5 | ||
|
|
8fc61a1c90 | ||
|
|
04691ed0da | ||
|
|
f9d4e5e077 | ||
|
|
4ee36f459f | ||
|
|
2c0de25423 | ||
|
|
c82bdbd39d | ||
|
|
71b08304f2 | ||
|
|
3bb3cd7c44 | ||
|
|
41187ec43d | ||
|
|
cf5de49deb | ||
|
|
19f177f3ee | ||
|
|
556d0cb1c5 | ||
|
|
7c7780b17e | ||
|
|
bd358b7692 | ||
|
|
1b8f180255 | ||
|
|
0bc90cea21 | ||
|
|
10f96ab23e | ||
|
|
4f0df507f4 | ||
|
|
256753e6ff | ||
|
|
8ac42078d4 | ||
|
|
fc83e054ea | ||
|
|
62cb304971 | ||
|
|
d4efebd694 | ||
|
|
7833f6bbc4 | ||
|
|
8b773c5f59 | ||
|
|
5767d5b95d | ||
|
|
13f1ecdbe3 | ||
|
|
8ae4ac47e6 | ||
|
|
001601bc5f | ||
|
|
3757507b18 | ||
|
|
9abab7aec3 | ||
|
|
1aabd537d8 | ||
|
|
6e240b3fd4 | ||
|
|
501a07c386 | ||
|
|
0d92dee14e | ||
|
|
3a1fa25106 | ||
|
|
b167770ea6 | ||
|
|
2a8a26ac54 | ||
|
|
9748833ba9 | ||
|
|
bf49214790 | ||
|
|
2b4410a55a | ||
|
|
bfd1b2eaf0 | ||
|
|
44c3421e92 | ||
|
|
7de3d3cc41 | ||
|
|
3d7f46da65 | ||
|
|
72d902e548 | ||
|
|
955fe76c3c | ||
|
|
839bec0363 | ||
|
|
9af4ce9a6b | ||
|
|
205bf75c7e | ||
|
|
bafbf3d841 | ||
|
|
1b0833fb45 | ||
|
|
0a15db2892 | ||
|
|
63dda10380 | ||
|
|
6e651df083 | ||
|
|
3a8fe24eec | ||
|
|
918a35e091 | ||
|
|
c76abeb8c0 | ||
|
|
d389b8ab38 | ||
|
|
a59faacbd7 | ||
|
|
12f9709601 | ||
|
|
455c9c02b9 | ||
|
|
1b49c6568b | ||
|
|
30e81c7699 | ||
|
|
2dafc68301 | ||
|
|
c310222af2 | ||
|
|
b4bb9a6603 | ||
|
|
279c621d23 | ||
|
|
eb11dbdcbd | ||
|
|
8dfdb34d31 | ||
|
|
fc9a73d67f | ||
|
|
4b5b84a0fc | ||
|
|
327c95d754 | ||
|
|
6e969e5d07 | ||
|
|
ca7c872420 | ||
|
|
8af4f42c42 | ||
|
|
ffce76a9b1 | ||
|
|
fca1d9dae4 | ||
|
|
eba09430e3 | ||
|
|
6bc8de7625 | ||
|
|
8a08ed8538 | ||
|
|
56d802f741 | ||
|
|
f7b46336fd | ||
|
|
510187c2ae | ||
|
|
ff6ff8db00 | ||
|
|
014017604d | ||
|
|
8cf544d54f | ||
|
|
870dd893fc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
dist
|
||||
@@ -10,7 +10,7 @@
|
||||
- Romain Beaumont <romain.rom1@gmail.com>
|
||||
- Dan Flettre <fletd01@yahoo.com>
|
||||
- Liam Gray <liam.r.gray@gmail.com>
|
||||
- grunjol <grunjol@argenteam.net>
|
||||
- grunjol <grunjol@users.noreply.github.com>
|
||||
- Rémi Jouannet <remijouannet@users.noreply.github.com>
|
||||
- Evan Miller <miller.evan815@gmail.com>
|
||||
- Alex <alxmorais8@msn.com>
|
||||
@@ -18,5 +18,9 @@
|
||||
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
|
||||
- gabriel <furstenheim@gmail.com>
|
||||
- Rolando Guedes <rolando.guedes@3gnt.net>
|
||||
- Benjamin Tan <demoneaux@gmail.com>
|
||||
- Mathias Rasmussen <mathiasvr@gmail.com>
|
||||
- Sergey Bargamon <sergey@bargamon.ru>
|
||||
- Thomas Watson Steen <w@tson.dk>
|
||||
|
||||
#### Generated by bin/update-authors.sh.
|
||||
|
||||
94
CHANGELOG.md
94
CHANGELOG.md
@@ -1,5 +1,99 @@
|
||||
# WebTorrent Desktop Version History
|
||||
|
||||
## v0.7.2 - 2016-06-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix exception that affects users upgrading from v0.5.1 or older
|
||||
- Ensure `state.saved.prefs` configuration exists
|
||||
- Fix window title on "About WebTorrent" window
|
||||
|
||||
## v0.7.1 - 2016-06-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Change "Step Forward" keyboard shortcut to `Alt+Left` (Windows)
|
||||
- Change "Step Backward" keyboard shortcut to to `Alt+Right` (Windows)
|
||||
|
||||
### Fixed
|
||||
|
||||
- First time startup bug -- invalid torrent/poster paths
|
||||
|
||||
## v0.7.0 - 2016-06-02
|
||||
|
||||
### Added
|
||||
|
||||
- Improved AirPlay support -- using the new [`airplayer`](https://www.npmjs.com/package/airplayer) package
|
||||
- Remember volume setting in player, for as long as the app is open
|
||||
|
||||
### Changed
|
||||
|
||||
- Add (+) button now also accepts non .torrent files and creates a torrent from
|
||||
those files
|
||||
- Show prompt text in title bar for open dialogs (OS X)
|
||||
- Upgrade Electron to 1.2.1
|
||||
- Improve window resizing when aspect ratio is enforced (OS X)
|
||||
- Use .ico format for better icon rendering quality (Windows)
|
||||
- Fix crash reporter not working (Windows)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Re-enable WebRTC (web peers)! (OS X, Windows)
|
||||
- Windows support was disabled in v0.6.1 to work around a bug in Electron
|
||||
- OS X support was disabled in v0.4.0 to work around a 100% CPU bug
|
||||
- Fix subtitle selector radio button UI size glitch
|
||||
- Fix race condition causing exeption on app startup
|
||||
- Fix duplicate torrent detection in some cases
|
||||
- Fix "gray screen" exception caused by incorrect file list order
|
||||
- Fix torrent loading message UI misalignment
|
||||
|
||||
### Known issues
|
||||
|
||||
- When upgrading to WebTorrent Desktop v0.7.0, some torrent metadata (file list,
|
||||
selected files, whether torrent is streamable) will be cleared. Just start the
|
||||
torrent to re-populate the metadata.
|
||||
|
||||
## v0.6.1 - 2016-05-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Disable WebRTC to work around Electron crash (Windows)
|
||||
- Will be re-enabled in the next version of WebTorrent, which will be based on
|
||||
the next version of Electron, where the bug is fixed.
|
||||
- Fix crash when updating from WebTorrent 0.5.x in some situtations (#583)
|
||||
- Fix crash when dropping files onto the dock icon (OS X)
|
||||
- Fix keyboard shortcuts Space and ESC being captured globally (#585)
|
||||
- Fix crash, show error when drag-dropping hidden files (#586)
|
||||
|
||||
## v0.6.0 - 2016-05-24
|
||||
|
||||
### Added
|
||||
|
||||
- Added Preferences page to set Download folder
|
||||
- Save video position, resume playback from saved position
|
||||
- Add additional video player keyboard shortcuts (#275)
|
||||
- Use `poster.jpg` file as the poster image if available (#558)
|
||||
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
|
||||
- Add support for pasting `instant.io` links (#559)
|
||||
- Add announcement feature
|
||||
|
||||
### Changed
|
||||
|
||||
- Nicer player UI
|
||||
- Reduce startup jank, improve startup time (#568)
|
||||
- Cleanup unsupported codec detection (#569, #570)
|
||||
- Cleaner look for the torrent file list
|
||||
- Improve subtitle positioning (#551)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
|
||||
- Fix bugs in LocationHistory
|
||||
- When player is active, and magnet link is pasted, go back to list
|
||||
- After deleting torrent, remove just the player from forward stack
|
||||
- After creating torrent, remove create torrent page from forward stack
|
||||
- Cancel button on create torrent page should only go back one page
|
||||
|
||||
## v0.5.1 - 2016-05-18
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -81,6 +81,10 @@ brew install wine
|
||||
|
||||
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
||||
|
||||
### Privacy
|
||||
|
||||
WebTorrent Desktop collects some basic usage stats to help us make the app better. For example, we track what OSs are users are on, and how well the play button works (how often does it succeed? time out? show a missing codec error?). The app never sends personally identifying or other private info.
|
||||
|
||||
### Code Style
|
||||
|
||||
[](https://github.com/feross/standard)
|
||||
|
||||
@@ -3,49 +3,97 @@
|
||||
var fs = require('fs')
|
||||
var cp = require('child_process')
|
||||
|
||||
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen']
|
||||
// We can't use `builtin-modules` here since our TravisCI
|
||||
// setup expects this file to run with no dependencies
|
||||
var BUILT_IN_NODE_MODULES = [
|
||||
'assert',
|
||||
'buffer',
|
||||
'child_process',
|
||||
'cluster',
|
||||
'console',
|
||||
'constants',
|
||||
'crypto',
|
||||
'dgram',
|
||||
'dns',
|
||||
'domain',
|
||||
'events',
|
||||
'fs',
|
||||
'http',
|
||||
'https',
|
||||
'module',
|
||||
'net',
|
||||
'os',
|
||||
'path',
|
||||
'process',
|
||||
'punycode',
|
||||
'querystring',
|
||||
'readline',
|
||||
'repl',
|
||||
'stream',
|
||||
'string_decoder',
|
||||
'timers',
|
||||
'tls',
|
||||
'tty',
|
||||
'url',
|
||||
'util',
|
||||
'v8',
|
||||
'vm',
|
||||
'zlib'
|
||||
]
|
||||
|
||||
var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
|
||||
|
||||
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
|
||||
|
||||
var EXECUTABLE_DEPS = ['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
|
||||
// Scans codebase for missing or unused dependencies. Exits with code 0 on success.
|
||||
function main () {
|
||||
if (process.platform === 'win32') {
|
||||
console.log('Sorry, check-deps only works on Mac and Linux')
|
||||
console.error('Sorry, check-deps only works on Mac and Linux')
|
||||
return
|
||||
}
|
||||
|
||||
var jsDeps = findJSDeps()
|
||||
var usedDeps = findUsedDeps()
|
||||
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)
|
||||
var missingDeps = usedDeps.filter(
|
||||
(dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep)
|
||||
)
|
||||
var unusedDeps = packageDeps.filter(
|
||||
(dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
|
||||
)
|
||||
|
||||
if (missingDeps.length > 0) console.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!')
|
||||
if (missingDeps.length > 0) {
|
||||
console.error('Missing package dependencies: ' + missingDeps)
|
||||
}
|
||||
if (unusedDeps.length > 0) {
|
||||
console.error('Unused package dependencies: ' + unusedDeps)
|
||||
}
|
||||
if (missingDeps.length + unusedDeps.length > 0) {
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Finds all dependencies, required, optional, or dev, in package.json
|
||||
// Finds all dependencies specified in `package.json`
|
||||
function findPackageDeps () {
|
||||
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||
var requiredDeps = Object.keys(pkg.dependencies)
|
||||
|
||||
var deps = Object.keys(pkg.dependencies)
|
||||
var devDeps = Object.keys(pkg.devDependencies)
|
||||
var optionalDeps = Object.keys(pkg.optionalDependencies)
|
||||
|
||||
return [].concat(requiredDeps, devDeps, optionalDeps)
|
||||
return [].concat(deps, devDeps, optionalDeps)
|
||||
}
|
||||
|
||||
// Finds all dependencies required() in the code
|
||||
function findJSDeps () {
|
||||
// Finds all dependencies that used with `require()`
|
||||
function findUsedDeps () {
|
||||
var stdout = cp.execSync('./bin/list-deps.sh')
|
||||
return stdout.toString().trim().split('\n')
|
||||
}
|
||||
|
||||
function includes (arr, elem) {
|
||||
return arr.indexOf(elem) >= 0
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ var path = require('path')
|
||||
|
||||
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
|
||||
child.on('close', function (code) {
|
||||
process.exit(code)
|
||||
process.exitCode = code
|
||||
})
|
||||
|
||||
@@ -2,7 +2,5 @@
|
||||
|
||||
var config = require('../config')
|
||||
var open = require('open')
|
||||
var path = require('path')
|
||||
|
||||
var configPath = path.join(config.CONFIG_PATH, 'config.json')
|
||||
open(configPath)
|
||||
open(config.CONFIG_PATH)
|
||||
|
||||
@@ -182,8 +182,6 @@ function buildDarwin (cb) {
|
||||
var infoPlistPath = path.join(contentsPath, 'Info.plist')
|
||||
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
|
||||
|
||||
// TODO: Use new `extend-info` and `extra-resource` opts to electron-packager,
|
||||
// available as of v6.
|
||||
infoPlist.CFBundleDocumentTypes = [
|
||||
{
|
||||
CFBundleTypeExtensions: [ 'torrent' ],
|
||||
@@ -211,6 +209,25 @@ function buildDarwin (cb) {
|
||||
}
|
||||
]
|
||||
|
||||
infoPlist.UTExportedTypeDeclarations = [
|
||||
{
|
||||
UTTypeConformsTo: [
|
||||
'public.data',
|
||||
'public.item',
|
||||
'com.bittorrent.torrent'
|
||||
],
|
||||
UTTypeDescription: 'BitTorrent Document',
|
||||
UTTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
||||
UTTypeIdentifier: 'org.bittorrent.torrent',
|
||||
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
|
||||
UTTypeTagSpecification: {
|
||||
'com.apple.ostype': 'TORR',
|
||||
'public.filename-extension': [ 'torrent' ],
|
||||
'public.mime-type': 'application/x-bittorrent'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
|
||||
|
||||
// Copy torrent file icon into app bundle
|
||||
@@ -310,11 +327,11 @@ function buildDarwin (cb) {
|
||||
}
|
||||
|
||||
var dmg = appDmg(dmgOpts)
|
||||
dmg.on('error', cb)
|
||||
dmg.once('error', cb)
|
||||
dmg.on('progress', function (info) {
|
||||
if (info.type === 'step-begin') console.log(info.title + '...')
|
||||
})
|
||||
dmg.on('finish', function (info) {
|
||||
dmg.once('finish', function (info) {
|
||||
console.log('OS X: Created dmg.')
|
||||
cb(null)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
npm run update-authors
|
||||
git diff --exit-code
|
||||
npm run package -- --sign
|
||||
git push
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
set -e
|
||||
|
||||
git pull
|
||||
npm run update-authors
|
||||
git diff --exit-code
|
||||
rm -rf node_modules/
|
||||
npm install
|
||||
npm dedupe
|
||||
|
||||
@@ -10,7 +10,6 @@ while (<>) {
|
||||
next if $seen{$_};
|
||||
next if /<support\@greenkeeper.io>/;
|
||||
next if /<ungoldman\@gmail.com>/;
|
||||
next if /<grunjol\@users.noreply.github.com>/;
|
||||
next if /<dc\@DCs-MacBook.local>/;
|
||||
next if /<rolandoguedes\@gmail.com>/;
|
||||
$seen{$_} = push @authors, "- ", $_;
|
||||
|
||||
57
config.js
57
config.js
@@ -10,6 +10,9 @@ var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings
|
||||
|
||||
module.exports = {
|
||||
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
|
||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
|
||||
|
||||
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
||||
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
||||
@@ -19,16 +22,40 @@ module.exports = {
|
||||
APP_VERSION: APP_VERSION,
|
||||
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
||||
|
||||
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
|
||||
|
||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||
|
||||
CONFIG_PATH: getConfigPath(),
|
||||
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||
|
||||
DEFAULT_TORRENTS: [
|
||||
{
|
||||
name: 'Big Buck Bunny',
|
||||
posterFileName: 'bigBuckBunny.jpg',
|
||||
torrentFileName: 'bigBuckBunny.torrent'
|
||||
},
|
||||
{
|
||||
name: 'Cosmos Laundromat (Preview)',
|
||||
posterFileName: 'cosmosLaundromat.jpg',
|
||||
torrentFileName: 'cosmosLaundromat.torrent'
|
||||
},
|
||||
{
|
||||
name: 'Sintel',
|
||||
posterFileName: 'sintel.jpg',
|
||||
torrentFileName: 'sintel.torrent'
|
||||
},
|
||||
{
|
||||
name: 'Tears of Steel',
|
||||
posterFileName: 'tearsOfSteel.jpg',
|
||||
torrentFileName: 'tearsOfSteel.torrent'
|
||||
},
|
||||
{
|
||||
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
||||
posterFileName: 'wiredCd.jpg',
|
||||
torrentFileName: 'wiredCd.torrent'
|
||||
}
|
||||
],
|
||||
|
||||
DELAYED_INIT: 3000 /* 3 seconds */,
|
||||
|
||||
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
|
||||
|
||||
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',
|
||||
@@ -38,8 +65,10 @@ module.exports = {
|
||||
IS_PORTABLE: isPortable(),
|
||||
IS_PRODUCTION: isProduction(),
|
||||
|
||||
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||
ROOT_PATH: __dirname,
|
||||
STATIC_PATH: path.join(__dirname, 'static'),
|
||||
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||
|
||||
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
||||
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
|
||||
@@ -57,6 +86,22 @@ function getConfigPath () {
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultDownloadPath () {
|
||||
if (!process || !process.type) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isPortable()) {
|
||||
return path.join(getConfigPath(), 'Downloads')
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
return process.type === 'renderer'
|
||||
? electron.remote.app.getPath('downloads')
|
||||
: electron.app.getPath('downloads')
|
||||
}
|
||||
|
||||
function isPortable () {
|
||||
try {
|
||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
||||
|
||||
@@ -3,7 +3,6 @@ module.exports = {
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
var get = require('simple-get')
|
||||
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
@@ -12,27 +11,47 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
||||
'?version=' + config.APP_VERSION +
|
||||
'&platform=' + process.platform
|
||||
|
||||
/**
|
||||
* In certain situations, the WebTorrent team may need to show an announcement to
|
||||
* all WebTorrent Desktop users. For example: a security notice, or an update
|
||||
* notification (if the auto-updater stops working).
|
||||
*
|
||||
* When there is an announcement, the `ANNOUNCEMENT_URL` endpoint should return an
|
||||
* HTTP 200 status code with a JSON object like this:
|
||||
*
|
||||
* {
|
||||
* "title": "WebTorrent Desktop Announcement",
|
||||
* "message": "Security Issue in v0.xx",
|
||||
* "detail": "Please update to v0.xx as soon as possible..."
|
||||
* }
|
||||
*/
|
||||
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 () {})
|
||||
})
|
||||
var get = require('simple-get')
|
||||
get.concat(ANNOUNCEMENT_URL, onResponse)
|
||||
}
|
||||
|
||||
function onResponse (err, res, data) {
|
||||
if (err) return log(`Failed to retrieve announcement: ${err.message}`)
|
||||
if (res.statusCode !== 200) return log('No announcement exists')
|
||||
|
||||
try {
|
||||
data = JSON.parse(data.toString())
|
||||
} catch (err) {
|
||||
// Support plaintext announcement messages, using a default title.
|
||||
data = {
|
||||
title: 'WebTorrent Desktop Announcement',
|
||||
message: data.toString(),
|
||||
detail: data.toString()
|
||||
}
|
||||
}
|
||||
|
||||
electron.dialog.showMessageBox({
|
||||
type: 'info',
|
||||
buttons: ['OK'],
|
||||
title: data.title,
|
||||
message: data.message,
|
||||
detail: data.detail
|
||||
}, noop)
|
||||
}
|
||||
|
||||
function noop () {}
|
||||
|
||||
122
main/dialog.js
Normal file
122
main/dialog.js
Normal file
@@ -0,0 +1,122 @@
|
||||
module.exports = {
|
||||
openSeedFile,
|
||||
openSeedDirectory,
|
||||
openTorrentFile,
|
||||
openTorrentAddress,
|
||||
openFiles
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
var windows = require('./windows')
|
||||
|
||||
/**
|
||||
* Show open dialog to create a single-file torrent.
|
||||
*/
|
||||
function openSeedFile () {
|
||||
if (!windows.main.win) return
|
||||
log('openSeedFile')
|
||||
var opts = {
|
||||
title: 'Select a file for the torrent.',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Show open dialog to create a single-file or single-directory torrent. On
|
||||
* Windows and Linux, open dialogs are for files *or* directories only, not both,
|
||||
* so this function shows a directory dialog on those platforms.
|
||||
*/
|
||||
function openSeedDirectory () {
|
||||
if (!windows.main.win) return
|
||||
log('openSeedDirectory')
|
||||
var opts = process.platform === 'darwin'
|
||||
? {
|
||||
title: 'Select a file or folder for the torrent.',
|
||||
properties: [ 'openFile', 'openDirectory' ]
|
||||
}
|
||||
: {
|
||||
title: 'Select a folder for the torrent.',
|
||||
properties: [ 'openDirectory' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Show flexible open dialog that supports selecting .torrent files to add, or
|
||||
* a file or folder to create a single-file or single-directory torrent.
|
||||
*/
|
||||
function openFiles () {
|
||||
if (!windows.main.win) return
|
||||
log('openFiles')
|
||||
var opts = process.platform === 'darwin'
|
||||
? {
|
||||
title: 'Select a file or folder to add.',
|
||||
properties: [ 'openFile', 'openDirectory' ]
|
||||
}
|
||||
: {
|
||||
title: 'Select a file to add.',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('onOpen', selectedPaths)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Show open dialog to open a .torrent file.
|
||||
*/
|
||||
function openTorrentFile () {
|
||||
if (!windows.main.win) return
|
||||
log('openTorrentFile')
|
||||
var opts = {
|
||||
title: 'Select a .torrent file.',
|
||||
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
||||
properties: [ 'openFile', 'multiSelections' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
selectedPaths.forEach(function (selectedPath) {
|
||||
windows.main.dispatch('addTorrent', selectedPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.)
|
||||
*/
|
||||
function openTorrentAddress () {
|
||||
log('openTorrentAddress')
|
||||
windows.main.dispatch('openTorrentAddress')
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialogs on do not show a title on OS X, so the window title is used instead.
|
||||
*/
|
||||
function setTitle (title) {
|
||||
if (process.platform === 'darwin') {
|
||||
windows.main.dispatch('setTitle', title)
|
||||
}
|
||||
}
|
||||
|
||||
function resetTitle () {
|
||||
setTitle(config.APP_WINDOW_TITLE)
|
||||
}
|
||||
59
main/dock.js
Normal file
59
main/dock.js
Normal file
@@ -0,0 +1,59 @@
|
||||
module.exports = {
|
||||
downloadFinished,
|
||||
init,
|
||||
setBadge
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
|
||||
var dialog = require('./dialog')
|
||||
var log = require('./log')
|
||||
|
||||
/**
|
||||
* Add a right-click menu to the dock icon. (OS X)
|
||||
*/
|
||||
function init () {
|
||||
if (!app.dock) return
|
||||
var menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
app.dock.setMenu(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounce the Downloads stack if `path` is inside the Downloads folder. (OS X)
|
||||
*/
|
||||
function downloadFinished (path) {
|
||||
if (!app.dock) return
|
||||
log(`downloadFinished: ${path}`)
|
||||
app.dock.downloadFinished(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Display string in dock badging area. (OS X)
|
||||
*/
|
||||
function setBadge (text) {
|
||||
if (!app.dock) return
|
||||
log(`setBadge: ${text}`)
|
||||
app.dock.setBadge(String(text))
|
||||
}
|
||||
|
||||
function getMenuTemplate () {
|
||||
return [
|
||||
{
|
||||
label: 'Create New Torrent...',
|
||||
accelerator: 'CmdOrCtrl+N',
|
||||
click: () => dialog.openSeedDirectory()
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent File...',
|
||||
accelerator: 'CmdOrCtrl+O',
|
||||
click: () => dialog.openTorrentFile()
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent Address...',
|
||||
accelerator: 'CmdOrCtrl+U',
|
||||
click: () => dialog.openTorrentAddress()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,9 +3,8 @@ module.exports = {
|
||||
uninstall
|
||||
}
|
||||
|
||||
var path = require('path')
|
||||
|
||||
var config = require('../config')
|
||||
var path = require('path')
|
||||
|
||||
function install () {
|
||||
if (process.platform === 'darwin') {
|
||||
@@ -35,11 +34,11 @@ function installDarwin () {
|
||||
var electron = require('electron')
|
||||
var app = electron.app
|
||||
|
||||
// On OS X, only protocols that are listed in Info.plist can be set as the default
|
||||
// handler at runtime.
|
||||
// On OS X, only protocols that are listed in `Info.plist` can be set as the
|
||||
// default handler at runtime.
|
||||
app.setAsDefaultProtocolClient('magnet')
|
||||
|
||||
// File handlers are registered in the Info.plist.
|
||||
// File handlers are defined in `Info.plist`.
|
||||
}
|
||||
|
||||
function uninstallDarwin () {}
|
||||
@@ -55,10 +54,22 @@ function installWin32 () {
|
||||
|
||||
var log = require('./log')
|
||||
|
||||
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
||||
|
||||
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
|
||||
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND)
|
||||
var iconPath = path.join(
|
||||
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
|
||||
)
|
||||
registerProtocolHandlerWin32(
|
||||
'magnet',
|
||||
'URL:BitTorrent Magnet URL',
|
||||
iconPath,
|
||||
EXEC_COMMAND
|
||||
)
|
||||
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:
|
||||
@@ -265,7 +276,9 @@ function installLinux () {
|
||||
installIconFile()
|
||||
|
||||
function installDesktopFile () {
|
||||
var templatePath = path.join(config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop')
|
||||
var templatePath = path.join(
|
||||
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
|
||||
)
|
||||
fs.readFile(templatePath, 'utf8', writeDesktopFile)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@ var ipcMain = electron.ipcMain
|
||||
var announcement = require('./announcement')
|
||||
var config = require('../config')
|
||||
var crashReporter = require('../crash-reporter')
|
||||
var dialog = require('./dialog')
|
||||
var dock = require('./dock')
|
||||
var handlers = require('./handlers')
|
||||
var ipc = require('./ipc')
|
||||
var log = require('./log')
|
||||
var menu = require('./menu')
|
||||
var shortcuts = require('./shortcuts')
|
||||
var squirrelWin32 = require('./squirrel-win32')
|
||||
var tray = require('./tray')
|
||||
var updater = require('./updater')
|
||||
@@ -54,23 +55,29 @@ function init () {
|
||||
|
||||
ipc.init()
|
||||
|
||||
app.on('will-finish-launching', function () {
|
||||
app.once('will-finish-launching', function () {
|
||||
crashReporter.init()
|
||||
})
|
||||
|
||||
app.on('ready', function () {
|
||||
isReady = true
|
||||
|
||||
windows.createMainWindow()
|
||||
windows.createWebTorrentHiddenWindow()
|
||||
windows.main.init()
|
||||
windows.webtorrent.init()
|
||||
menu.init()
|
||||
shortcuts.init()
|
||||
|
||||
// To keep app startup fast, some code is delayed.
|
||||
setTimeout(delayedInit, config.DELAYED_INIT)
|
||||
|
||||
// Report uncaught exceptions
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error(err)
|
||||
var errJSON = {message: err.message, stack: err.stack}
|
||||
windows.main.dispatch('uncaughtError', 'main', errJSON)
|
||||
})
|
||||
})
|
||||
|
||||
app.on('ipcReady', function () {
|
||||
app.once('ipcReady', function () {
|
||||
log('Command line args:', argv)
|
||||
processArgv(argv)
|
||||
console.timeEnd('init')
|
||||
@@ -81,20 +88,21 @@ function init () {
|
||||
|
||||
app.isQuitting = true
|
||||
e.preventDefault()
|
||||
windows.main.send('dispatch', 'saveState') /* try to save state on exit */
|
||||
windows.main.dispatch('saveState') // try to save state on exit
|
||||
ipcMain.once('savedState', () => app.quit())
|
||||
setTimeout(() => app.quit(), 2000) /* quit after 2 secs, at most */
|
||||
setTimeout(() => app.quit(), 2000) // quit after 2 secs, at most
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
if (isReady) windows.createMainWindow()
|
||||
if (isReady) windows.main.show()
|
||||
})
|
||||
}
|
||||
|
||||
function delayedInit () {
|
||||
announcement.init()
|
||||
tray.init()
|
||||
dock.init()
|
||||
handlers.install()
|
||||
tray.init()
|
||||
updater.init()
|
||||
}
|
||||
|
||||
@@ -102,13 +110,12 @@ function onOpen (e, torrentId) {
|
||||
e.preventDefault()
|
||||
|
||||
if (app.ipcReady) {
|
||||
windows.main.send('dispatch', 'onOpen', torrentId)
|
||||
// Magnet links opened from Chrome won't focus the app without a setTimeout. The
|
||||
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
|
||||
// Magnet links opened from Chrome won't focus the app without a setTimeout.
|
||||
// The confirmation dialog Chrome shows causes Chrome to steal back the focus.
|
||||
// Electron issue: https://github.com/atom/electron/issues/4338
|
||||
setTimeout(function () {
|
||||
windows.focusWindow(windows.main)
|
||||
}, 100)
|
||||
setTimeout(() => windows.main.show(), 100)
|
||||
|
||||
processArgv([ torrentId ])
|
||||
} else {
|
||||
argv.push(torrentId)
|
||||
}
|
||||
@@ -119,7 +126,7 @@ function onAppOpen (newArgv) {
|
||||
|
||||
if (app.ipcReady) {
|
||||
log('Second app instance opened, but was prevented:', newArgv)
|
||||
windows.focusWindow(windows.main)
|
||||
windows.main.show()
|
||||
|
||||
processArgv(newArgv)
|
||||
} else {
|
||||
@@ -132,18 +139,22 @@ function sliceArgv (argv) {
|
||||
}
|
||||
|
||||
function processArgv (argv) {
|
||||
var torrentIds = []
|
||||
argv.forEach(function (arg) {
|
||||
if (arg === '-n') {
|
||||
menu.showOpenSeedFiles()
|
||||
dialog.openSeedDirectory()
|
||||
} else if (arg === '-o') {
|
||||
menu.showOpenTorrentFile()
|
||||
dialog.openTorrentFile()
|
||||
} else if (arg === '-u') {
|
||||
menu.showOpenTorrentAddress()
|
||||
dialog.openTorrentAddress()
|
||||
} else if (arg.startsWith('-psn')) {
|
||||
// Ignore OS X launchd "process serial number" argument
|
||||
// More: https://github.com/feross/webtorrent-desktop/issues/214
|
||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
||||
} else {
|
||||
windows.main.send('dispatch', 'onOpen', arg)
|
||||
torrentIds.push(arg)
|
||||
}
|
||||
})
|
||||
if (torrentIds.length > 0) {
|
||||
windows.main.dispatch('onOpen', torrentIds)
|
||||
}
|
||||
}
|
||||
|
||||
218
main/ipc.js
218
main/ipc.js
@@ -5,31 +5,33 @@ module.exports = {
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
var ipcMain = electron.ipcMain
|
||||
|
||||
var dialog = require('./dialog')
|
||||
var dock = require('./dock')
|
||||
var log = require('./log')
|
||||
var menu = require('./menu')
|
||||
var windows = require('./windows')
|
||||
var powerSaveBlocker = require('./power-save-blocker')
|
||||
var shell = require('./shell')
|
||||
var shortcuts = require('./shortcuts')
|
||||
var vlc = require('./vlc')
|
||||
var windows = require('./windows')
|
||||
var thumbnail = require('./thumbnail')
|
||||
|
||||
// has to be a number, not a boolean, and undefined throws an error
|
||||
var powerSaveBlockerId = 0
|
||||
|
||||
// messages from the main process, to be sent once the WebTorrent process starts
|
||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||
var messageQueueMainToWebTorrent = []
|
||||
|
||||
// holds a ChildProcess while we're playing a video in VLC, null otherwise
|
||||
var vlcProcess
|
||||
|
||||
function init () {
|
||||
ipcMain.on('ipcReady', function (e) {
|
||||
windows.main.show()
|
||||
var ipc = electron.ipcMain
|
||||
|
||||
ipc.once('ipcReady', function (e) {
|
||||
app.ipcReady = true
|
||||
app.emit('ipcReady')
|
||||
})
|
||||
|
||||
ipcMain.on('ipcReadyWebTorrent', function (e) {
|
||||
ipc.once('ipcReadyWebTorrent', function (e) {
|
||||
app.ipcReadyWebTorrent = true
|
||||
log('sending %d queued messages from the main win to the webtorrent window',
|
||||
messageQueueMainToWebTorrent.length)
|
||||
@@ -39,113 +41,117 @@ function init () {
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
|
||||
/**
|
||||
* Dialog
|
||||
*/
|
||||
|
||||
ipcMain.on('setBounds', function (e, bounds, maximize) {
|
||||
setBounds(bounds, maximize)
|
||||
})
|
||||
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
|
||||
ipc.on('openFiles', () => dialog.openFiles())
|
||||
|
||||
ipcMain.on('setAspectRatio', function (e, aspectRatio) {
|
||||
setAspectRatio(aspectRatio)
|
||||
})
|
||||
/**
|
||||
* Dock
|
||||
*/
|
||||
|
||||
ipcMain.on('setBadge', function (e, text) {
|
||||
setBadge(text)
|
||||
})
|
||||
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
|
||||
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
|
||||
|
||||
ipcMain.on('setProgress', function (e, progress) {
|
||||
setProgress(progress)
|
||||
})
|
||||
/**
|
||||
* Events
|
||||
*/
|
||||
|
||||
ipcMain.on('toggleFullScreen', function (e, flag) {
|
||||
menu.toggleFullScreen(flag)
|
||||
})
|
||||
|
||||
ipcMain.on('setTitle', function (e, title) {
|
||||
windows.main.setTitle(title)
|
||||
})
|
||||
|
||||
ipcMain.on('openItem', function (e, path) {
|
||||
log('open item: ' + 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('unblockPowerSave', unblockPowerSave)
|
||||
|
||||
ipcMain.on('onPlayerOpen', function () {
|
||||
ipc.on('onPlayerOpen', function () {
|
||||
menu.onPlayerOpen()
|
||||
shortcuts.onPlayerOpen()
|
||||
})
|
||||
|
||||
ipcMain.on('onPlayerClose', function () {
|
||||
ipc.on('onPlayerClose', function () {
|
||||
menu.onPlayerClose()
|
||||
shortcuts.onPlayerOpen()
|
||||
})
|
||||
|
||||
ipcMain.on('focusWindow', function (e, windowName) {
|
||||
windows.focusWindow(windows[windowName])
|
||||
ipc.on('updateThumbnailBar', function (e, isPaused) {
|
||||
thumbnail.updateThumbarButtons(isPaused)
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
/**
|
||||
* Power Save Blocker
|
||||
*/
|
||||
|
||||
ipcMain.on('checkForVLC', function (e) {
|
||||
ipc.on('blockPowerSave', () => powerSaveBlocker.start())
|
||||
ipc.on('unblockPowerSave', () => powerSaveBlocker.stop())
|
||||
|
||||
/**
|
||||
* Shell
|
||||
*/
|
||||
|
||||
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
||||
|
||||
/**
|
||||
* Windows: Main
|
||||
*/
|
||||
|
||||
var main = windows.main
|
||||
|
||||
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
|
||||
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
|
||||
ipc.on('setProgress', (e, ...args) => main.setProgress(...args))
|
||||
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
||||
ipc.on('show', () => main.show())
|
||||
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
||||
|
||||
/**
|
||||
* VLC
|
||||
* TODO: Move most of this code to vlc.js
|
||||
*/
|
||||
|
||||
ipc.on('checkForVLC', function (e) {
|
||||
vlc.checkForVLC(function (isInstalled) {
|
||||
windows.main.send('checkForVLC', isInstalled)
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('vlcPlay', function (e, url) {
|
||||
ipc.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(' '))
|
||||
log('Running vlc ' + args.join(' '))
|
||||
|
||||
vlc.spawn(args, function (err, proc) {
|
||||
if (err) return windows.main.send('dispatch', 'vlcNotFound')
|
||||
if (err) return windows.main.dispatch('vlcNotFound')
|
||||
vlcProcess = proc
|
||||
|
||||
// If it works, close the modal after a second
|
||||
var closeModalTimeout = setTimeout(() =>
|
||||
windows.main.send('dispatch', 'exitModal'), 1000)
|
||||
windows.main.dispatch('exitModal'), 1000)
|
||||
|
||||
vlcProcess.on('close', function (code) {
|
||||
clearTimeout(closeModalTimeout)
|
||||
if (!vlcProcess) return // Killed
|
||||
console.log('VLC exited with code ', code)
|
||||
log('VLC exited with code ', code)
|
||||
if (code === 0) {
|
||||
windows.main.send('dispatch', 'backToList')
|
||||
windows.main.dispatch('backToList')
|
||||
} else {
|
||||
windows.main.send('dispatch', 'vlcNotFound')
|
||||
windows.main.dispatch('vlcNotFound')
|
||||
}
|
||||
vlcProcess = null
|
||||
})
|
||||
|
||||
vlcProcess.on('error', function (e) {
|
||||
console.log('VLC error', e)
|
||||
log('VLC error', e)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('vlcQuit', function () {
|
||||
ipc.on('vlcQuit', function () {
|
||||
if (!vlcProcess) return
|
||||
console.log('Killing VLC, pid ' + vlcProcess.pid)
|
||||
log('Killing VLC, pid ' + vlcProcess.pid)
|
||||
vlcProcess.kill('SIGKILL') // kill -9
|
||||
vlcProcess = null
|
||||
})
|
||||
|
||||
// Capture all events
|
||||
var oldEmit = ipcMain.emit
|
||||
ipcMain.emit = function (name, e, ...args) {
|
||||
var oldEmit = ipc.emit
|
||||
ipc.emit = function (name, e, ...args) {
|
||||
// Relay messages between the main window and the WebTorrent hidden window
|
||||
if (name.startsWith('wt-') && !app.isQuitting) {
|
||||
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
|
||||
@@ -168,82 +174,6 @@ function init () {
|
||||
}
|
||||
|
||||
// Emit all other events normally
|
||||
oldEmit.call(ipcMain, name, e, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
function setBounds (bounds, maximize) {
|
||||
// Do nothing in fullscreen
|
||||
if (!windows.main || windows.main.isFullScreen()) {
|
||||
log('setBounds: not setting bounds because we\'re in full screen')
|
||||
return
|
||||
}
|
||||
|
||||
// Maximize or minimize, if the second argument is present
|
||||
var willBeMaximized
|
||||
if (maximize === true) {
|
||||
if (!windows.main.isMaximized()) {
|
||||
log('setBounds: maximizing')
|
||||
windows.main.maximize()
|
||||
}
|
||||
willBeMaximized = true
|
||||
} else if (maximize === false) {
|
||||
if (windows.main.isMaximized()) {
|
||||
log('setBounds: unmaximizing')
|
||||
windows.main.unmaximize()
|
||||
}
|
||||
willBeMaximized = false
|
||||
} else {
|
||||
willBeMaximized = windows.main.isMaximized()
|
||||
}
|
||||
|
||||
// Assuming we're not maximized or maximizing, set the window size
|
||||
if (!willBeMaximized) {
|
||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
||||
if (bounds.x === null && bounds.y === null) {
|
||||
// X and Y not specified? By default, center on current screen
|
||||
var scr = electron.screen.getDisplayMatching(windows.main.getBounds())
|
||||
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||
}
|
||||
windows.main.setBounds(bounds, true)
|
||||
} else {
|
||||
log('setBounds: not setting bounds because of window maximization')
|
||||
}
|
||||
}
|
||||
|
||||
function setAspectRatio (aspectRatio) {
|
||||
log('setAspectRatio %o', aspectRatio)
|
||||
if (windows.main) {
|
||||
windows.main.setAspectRatio(aspectRatio)
|
||||
}
|
||||
}
|
||||
|
||||
// Display string in dock badging area (OS X)
|
||||
function setBadge (text) {
|
||||
log('setBadge %s', text)
|
||||
if (app.dock) {
|
||||
app.dock.setBadge(String(text))
|
||||
}
|
||||
}
|
||||
|
||||
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
|
||||
function setProgress (progress) {
|
||||
log('setProgress %s', progress)
|
||||
if (windows.main) {
|
||||
windows.main.setProgressBar(progress)
|
||||
}
|
||||
}
|
||||
|
||||
function blockPowerSave () {
|
||||
powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||
log('blockPowerSave %d', powerSaveBlockerId)
|
||||
}
|
||||
|
||||
function unblockPowerSave () {
|
||||
if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) {
|
||||
electron.powerSaveBlocker.stop(powerSaveBlockerId)
|
||||
log('unblockPowerSave %d', powerSaveBlockerId)
|
||||
oldEmit.call(ipc, name, e, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ module.exports.error = error
|
||||
*/
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var windows = require('./windows')
|
||||
|
||||
var app = electron.app
|
||||
@@ -18,7 +17,7 @@ function log (...args) {
|
||||
if (app.ipcReady) {
|
||||
windows.main.send('log', ...args)
|
||||
} else {
|
||||
app.on('ipcReady', () => windows.main.send('log', ...args))
|
||||
app.once('ipcReady', () => windows.main.send('log', ...args))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +25,6 @@ function error (...args) {
|
||||
if (app.ipcReady) {
|
||||
windows.main.send('error', ...args)
|
||||
} else {
|
||||
app.on('ipcReady', () => windows.main.send('error', ...args))
|
||||
app.once('ipcReady', () => windows.main.send('error', ...args))
|
||||
}
|
||||
}
|
||||
|
||||
306
main/menu.js
306
main/menu.js
@@ -2,15 +2,10 @@ module.exports = {
|
||||
init,
|
||||
onPlayerClose,
|
||||
onPlayerOpen,
|
||||
onToggleAlwaysOnTop,
|
||||
onToggleFullScreen,
|
||||
onWindowHide,
|
||||
onWindowShow,
|
||||
|
||||
// TODO: move these out of menu.js -- they don't belong here
|
||||
showOpenSeedFiles,
|
||||
showOpenTorrentAddress,
|
||||
showOpenTorrentFile,
|
||||
toggleFullScreen
|
||||
onWindowBlur,
|
||||
onWindowFocus
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
@@ -18,170 +13,72 @@ var electron = require('electron')
|
||||
var app = electron.app
|
||||
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
var dialog = require('./dialog')
|
||||
var shell = require('./shell')
|
||||
var windows = require('./windows')
|
||||
var thumbnail = require('./thumbnail')
|
||||
|
||||
var appMenu
|
||||
var menu
|
||||
|
||||
function init () {
|
||||
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
|
||||
electron.Menu.setApplicationMenu(appMenu)
|
||||
|
||||
if (app.dock) {
|
||||
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
|
||||
app.dock.setMenu(dockMenu)
|
||||
}
|
||||
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
electron.Menu.setApplicationMenu(menu)
|
||||
}
|
||||
|
||||
function toggleFullScreen (flag) {
|
||||
log('toggleFullScreen %s', flag)
|
||||
if (windows.main && windows.main.isVisible()) {
|
||||
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)
|
||||
}
|
||||
function onPlayerClose () {
|
||||
getMenuItem('Play/Pause').enabled = false
|
||||
getMenuItem('Increase Volume').enabled = false
|
||||
getMenuItem('Decrease Volume').enabled = false
|
||||
getMenuItem('Step Forward').enabled = false
|
||||
getMenuItem('Step Backward').enabled = false
|
||||
getMenuItem('Increase Speed').enabled = false
|
||||
getMenuItem('Decrease Speed').enabled = false
|
||||
getMenuItem('Add Subtitles File...').enabled = false
|
||||
|
||||
thumbnail.showPlayerThumbnailBar()
|
||||
}
|
||||
|
||||
// Sets whether the window should always show on top of other windows
|
||||
function toggleFloatOnTop (flag) {
|
||||
log('toggleFloatOnTop %s', flag)
|
||||
if (windows.main) {
|
||||
flag = flag != null ? flag : !windows.main.isAlwaysOnTop()
|
||||
windows.main.setAlwaysOnTop(flag)
|
||||
getMenuItem('Float on Top').checked = flag
|
||||
}
|
||||
function onPlayerOpen () {
|
||||
getMenuItem('Play/Pause').enabled = true
|
||||
getMenuItem('Increase Volume').enabled = true
|
||||
getMenuItem('Decrease Volume').enabled = true
|
||||
getMenuItem('Step Forward').enabled = true
|
||||
getMenuItem('Step Backward').enabled = true
|
||||
getMenuItem('Increase Speed').enabled = true
|
||||
getMenuItem('Decrease Speed').enabled = true
|
||||
getMenuItem('Add Subtitles File...').enabled = true
|
||||
|
||||
thumbnail.hidePlayerThumbnailBar()
|
||||
}
|
||||
|
||||
function toggleDevTools () {
|
||||
log('toggleDevTools')
|
||||
if (windows.main) {
|
||||
windows.main.toggleDevTools()
|
||||
}
|
||||
function onToggleAlwaysOnTop (flag) {
|
||||
getMenuItem('Float on Top').checked = flag
|
||||
}
|
||||
|
||||
function showWebTorrentWindow () {
|
||||
log('showWebTorrentWindow')
|
||||
windows.webtorrent.show()
|
||||
windows.webtorrent.webContents.openDevTools({ detach: true })
|
||||
function onToggleFullScreen (flag) {
|
||||
getMenuItem('Full Screen').checked = flag
|
||||
}
|
||||
|
||||
function playPause () {
|
||||
if (windows.main) {
|
||||
windows.main.send('dispatch', 'playPause')
|
||||
}
|
||||
}
|
||||
|
||||
function increaseVolume () {
|
||||
if (windows.main) {
|
||||
windows.main.send('dispatch', 'changeVolume', 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
function decreaseVolume () {
|
||||
if (windows.main) {
|
||||
windows.main.send('dispatch', 'changeVolume', -0.1)
|
||||
}
|
||||
}
|
||||
|
||||
function openSubtitles () {
|
||||
if (windows.main) {
|
||||
windows.main.send('dispatch', 'openSubtitles')
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowShow () {
|
||||
log('onWindowShow')
|
||||
getMenuItem('Full Screen').enabled = true
|
||||
getMenuItem('Float on Top').enabled = true
|
||||
}
|
||||
|
||||
function onWindowHide () {
|
||||
log('onWindowHide')
|
||||
function onWindowBlur () {
|
||||
getMenuItem('Full Screen').enabled = false
|
||||
getMenuItem('Float on Top').enabled = false
|
||||
}
|
||||
|
||||
function onPlayerOpen () {
|
||||
log('onPlayerOpen')
|
||||
getMenuItem('Play/Pause').enabled = true
|
||||
getMenuItem('Increase Volume').enabled = true
|
||||
getMenuItem('Decrease Volume').enabled = true
|
||||
getMenuItem('Add Subtitles File...').enabled = true
|
||||
}
|
||||
|
||||
function onPlayerClose () {
|
||||
log('onPlayerClose')
|
||||
getMenuItem('Play/Pause').enabled = false
|
||||
getMenuItem('Increase Volume').enabled = false
|
||||
getMenuItem('Decrease Volume').enabled = false
|
||||
getMenuItem('Add Subtitles File...').enabled = false
|
||||
}
|
||||
|
||||
function onToggleFullScreen (isFullScreen) {
|
||||
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen()
|
||||
windows.main.setMenuBarVisibility(!isFullScreen)
|
||||
getMenuItem('Full Screen').checked = isFullScreen
|
||||
windows.main.send('fullscreenChanged', isFullScreen)
|
||||
function onWindowFocus () {
|
||||
getMenuItem('Full Screen').enabled = true
|
||||
getMenuItem('Float on Top').enabled = true
|
||||
}
|
||||
|
||||
function getMenuItem (label) {
|
||||
for (var i = 0; i < appMenu.items.length; i++) {
|
||||
var menuItem = appMenu.items[i].submenu.items.find(function (item) {
|
||||
for (var i = 0; i < menu.items.length; i++) {
|
||||
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||
return item.label === label
|
||||
})
|
||||
if (menuItem) return menuItem
|
||||
}
|
||||
}
|
||||
|
||||
// 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 () {
|
||||
electron.dialog.showOpenDialog({
|
||||
title: 'Select a file or folder for the torrent file.',
|
||||
properties: [ 'openFile', 'openDirectory' ]
|
||||
}, function (selectedPaths) {
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
var selectedPath = selectedPaths[0]
|
||||
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
|
||||
})
|
||||
}
|
||||
|
||||
// Prompts the user to choose a torrent file, then adds it to the app
|
||||
function showOpenTorrentFile () {
|
||||
electron.dialog.showOpenDialog(windows.main, {
|
||||
title: 'Select a .torrent file to open.',
|
||||
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
||||
properties: [ 'openFile', 'multiSelections' ]
|
||||
}, function (selectedPaths) {
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
selectedPaths.forEach(function (selectedPath) {
|
||||
windows.main.send('dispatch', 'addTorrent', selectedPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Prompts the user for the URL of a torrent file, then downloads and adds it
|
||||
function showOpenTorrentAddress () {
|
||||
windows.main.send('showOpenTorrentAddress')
|
||||
}
|
||||
|
||||
function getAppMenuTemplate () {
|
||||
function getMenuTemplate () {
|
||||
var template = [
|
||||
{
|
||||
label: 'File',
|
||||
@@ -191,17 +88,17 @@ function getAppMenuTemplate () {
|
||||
? 'Create New Torrent...'
|
||||
: 'Create New Torrent from Folder...',
|
||||
accelerator: 'CmdOrCtrl+N',
|
||||
click: showOpenSeedFiles
|
||||
click: () => dialog.openSeedDirectory()
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent File...',
|
||||
accelerator: 'CmdOrCtrl+O',
|
||||
click: showOpenTorrentFile
|
||||
click: () => dialog.openTorrentFile()
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent Address...',
|
||||
accelerator: 'CmdOrCtrl+U',
|
||||
click: showOpenTorrentAddress
|
||||
click: () => dialog.openTorrentAddress()
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
@@ -237,6 +134,14 @@ function getAppMenuTemplate () {
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
role: 'selectall'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
click: () => windows.main.dispatch('preferences')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -249,12 +154,20 @@ function getAppMenuTemplate () {
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'Ctrl+Command+F'
|
||||
: 'F11',
|
||||
click: () => toggleFullScreen()
|
||||
click: () => windows.main.toggleFullScreen()
|
||||
},
|
||||
{
|
||||
label: 'Float on Top',
|
||||
type: 'checkbox',
|
||||
click: () => toggleFloatOnTop()
|
||||
click: () => windows.main.toggleAlwaysOnTop()
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Go Back',
|
||||
accelerator: 'Esc',
|
||||
click: () => windows.main.dispatch('escapeBack')
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
@@ -267,14 +180,14 @@ function getAppMenuTemplate () {
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'Alt+Command+I'
|
||||
: 'Ctrl+Shift+I',
|
||||
click: toggleDevTools
|
||||
click: () => windows.main.toggleDevTools()
|
||||
},
|
||||
{
|
||||
label: 'Show WebTorrent Process',
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'Alt+Command+P'
|
||||
: 'Ctrl+Shift+P',
|
||||
click: showWebTorrentWindow
|
||||
click: () => windows.webtorrent.toggleDevTools()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -285,8 +198,8 @@ function getAppMenuTemplate () {
|
||||
submenu: [
|
||||
{
|
||||
label: 'Play/Pause',
|
||||
accelerator: 'CmdOrCtrl+P',
|
||||
click: playPause,
|
||||
accelerator: 'Space',
|
||||
click: () => windows.main.dispatch('playPause'),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
@@ -295,13 +208,47 @@ function getAppMenuTemplate () {
|
||||
{
|
||||
label: 'Increase Volume',
|
||||
accelerator: 'CmdOrCtrl+Up',
|
||||
click: increaseVolume,
|
||||
click: () => windows.main.dispatch('changeVolume', 0.1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Decrease Volume',
|
||||
accelerator: 'CmdOrCtrl+Down',
|
||||
click: decreaseVolume,
|
||||
click: () => windows.main.dispatch('changeVolume', -0.1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Step Forward',
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'CmdOrCtrl+Alt+Right'
|
||||
: 'Alt+Right',
|
||||
click: () => windows.main.dispatch('skip', 1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Step Backward',
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'CmdOrCtrl+Alt+Left'
|
||||
: 'Alt+Left',
|
||||
click: () => windows.main.dispatch('skip', -1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Increase Speed',
|
||||
accelerator: 'CmdOrCtrl+=',
|
||||
click: () => windows.main.dispatch('changePlaybackRate', 1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Decrease Speed',
|
||||
accelerator: 'CmdOrCtrl+-',
|
||||
click: () => windows.main.dispatch('changePlaybackRate', -1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
@@ -309,7 +256,7 @@ function getAppMenuTemplate () {
|
||||
},
|
||||
{
|
||||
label: 'Add Subtitles File...',
|
||||
click: openSubtitles,
|
||||
click: () => windows.main.dispatch('openSubtitles'),
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
@@ -320,18 +267,18 @@ function getAppMenuTemplate () {
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn more about ' + config.APP_NAME,
|
||||
click: () => electron.shell.openExternal(config.HOME_PAGE_URL)
|
||||
click: () => shell.openExternal(config.HOME_PAGE_URL)
|
||||
},
|
||||
{
|
||||
label: 'Contribute on GitHub',
|
||||
click: () => electron.shell.openExternal(config.GITHUB_URL)
|
||||
click: () => shell.openExternal(config.GITHUB_URL)
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Report an Issue...',
|
||||
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -349,6 +296,14 @@ function getAppMenuTemplate () {
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'Cmd+,',
|
||||
click: () => windows.main.dispatch('preferences')
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Services',
|
||||
role: 'services',
|
||||
@@ -403,12 +358,13 @@ function getAppMenuTemplate () {
|
||||
})
|
||||
}
|
||||
|
||||
// In Linux and Windows it is not possible to open both folders and files
|
||||
// On Windows and Linux, open dialogs do not support selecting both files and
|
||||
// folders and files, so add an extra menu item so there is one for each type.
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
// File menu (Windows, Linux)
|
||||
template[0].submenu.unshift({
|
||||
label: 'Create New Torrent from File...',
|
||||
click: showOpenSeedFile
|
||||
click: () => dialog.openSeedFile()
|
||||
})
|
||||
|
||||
// Help menu (Windows, Linux)
|
||||
@@ -418,12 +374,12 @@ function getAppMenuTemplate () {
|
||||
},
|
||||
{
|
||||
label: 'About ' + config.APP_NAME,
|
||||
click: windows.createAboutWindow
|
||||
click: () => windows.about.init()
|
||||
}
|
||||
)
|
||||
}
|
||||
// Add "File > Quit" menu item so Linux distros where the system tray icon is missing
|
||||
// will have a way to quit the app.
|
||||
// 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({
|
||||
@@ -434,23 +390,3 @@ function getAppMenuTemplate () {
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
function getDockMenuTemplate () {
|
||||
return [
|
||||
{
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
30
main/power-save-blocker.js
Normal file
30
main/power-save-blocker.js
Normal file
@@ -0,0 +1,30 @@
|
||||
module.exports = {
|
||||
start,
|
||||
stop
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
var log = require('./log')
|
||||
|
||||
var blockId = 0
|
||||
|
||||
/**
|
||||
* Block the system from entering low-power (sleep) mode or turning off the
|
||||
* display.
|
||||
*/
|
||||
function start () {
|
||||
stop() // Stop the previous power saver block, if one exists.
|
||||
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||
log(`powerSaveBlocker.start: ${blockId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop blocking the system from entering low-power mode.
|
||||
*/
|
||||
function stop () {
|
||||
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
||||
return
|
||||
}
|
||||
electron.powerSaveBlocker.stop(blockId)
|
||||
log(`powerSaveBlocker.stop: ${blockId}`)
|
||||
}
|
||||
41
main/shell.js
Normal file
41
main/shell.js
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
openExternal,
|
||||
openItem,
|
||||
showItemInFolder,
|
||||
moveItemToTrash
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
var log = require('./log')
|
||||
|
||||
/**
|
||||
* Open the given external protocol URL in the desktop’s default manner.
|
||||
*/
|
||||
function openExternal (url) {
|
||||
log(`openExternal: ${url}`)
|
||||
electron.shell.openExternal(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given file in the desktop’s default manner.
|
||||
*/
|
||||
function openItem (path) {
|
||||
log(`openItem: ${path}`)
|
||||
electron.shell.openItem(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the given file in a file manager. If possible, select the file.
|
||||
*/
|
||||
function showItemInFolder (path) {
|
||||
log(`showItemInFolder: ${path}`)
|
||||
electron.shell.showItemInFolder(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given file to trash and returns a boolean status for the operation.
|
||||
*/
|
||||
function moveItemToTrash (path) {
|
||||
log(`moveItemToTrash: ${path}`)
|
||||
electron.shell.moveItemToTrash(path)
|
||||
}
|
||||
@@ -1,34 +1,20 @@
|
||||
module.exports = {
|
||||
init,
|
||||
onPlayerClose,
|
||||
onPlayerOpen
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var menu = require('./menu')
|
||||
var windows = require('./windows')
|
||||
|
||||
function init () {
|
||||
var localShortcut = require('electron-localshortcut')
|
||||
|
||||
// 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('Space', () => windows.main.send('dispatch', 'playPause'))
|
||||
|
||||
// Hidden shortcuts, i.e. not shown in the menu
|
||||
localShortcut.register('Esc', () => windows.main.send('dispatch', 'escapeBack'))
|
||||
}
|
||||
|
||||
function onPlayerOpen () {
|
||||
// Register special "media key" for play/pause, available on some keyboards
|
||||
// Register play/pause media key, available on some keyboards.
|
||||
electron.globalShortcut.register(
|
||||
'MediaPlayPause',
|
||||
() => windows.main.send('dispatch', 'playPause')
|
||||
() => windows.main.dispatch('playPause')
|
||||
)
|
||||
}
|
||||
|
||||
function onPlayerClose () {
|
||||
// Return the media key to the OS, so other apps can use it.
|
||||
electron.globalShortcut.unregister('MediaPlayPause')
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ var app = electron.app
|
||||
|
||||
var handlers = require('./handlers')
|
||||
|
||||
var exeName = path.basename(process.execPath)
|
||||
var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||
var EXE_NAME = path.basename(process.execPath)
|
||||
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||
|
||||
function handleEvent (cmd) {
|
||||
if (cmd === '--squirrel-install') {
|
||||
@@ -61,15 +61,17 @@ function handleEvent (cmd) {
|
||||
}
|
||||
|
||||
if (cmd === '--squirrel-firstrun') {
|
||||
// This is called on the app's first run. Do not quit, allow startup to continue.
|
||||
// App is running for the first time. Do not quit, allow startup to continue.
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Spawn a command and invoke the callback when it completes with an error and the output
|
||||
// from standard out.
|
||||
/**
|
||||
* Spawn a command and invoke the callback when it completes with an error and
|
||||
* the output from standard out.
|
||||
*/
|
||||
function spawn (command, args, cb) {
|
||||
var stdout = ''
|
||||
|
||||
@@ -99,24 +101,31 @@ function spawn (command, args, cb) {
|
||||
})
|
||||
}
|
||||
|
||||
// Spawn Squirrel's Update.exe with the given arguments and invoke the callback when the
|
||||
// command completes.
|
||||
/**
|
||||
* Spawn the Squirrel `Update.exe` command with the given arguments and invoke
|
||||
* the callback when the command completes.
|
||||
*/
|
||||
function spawnUpdate (args, cb) {
|
||||
spawn(updateDotExe, args, cb)
|
||||
spawn(UPDATE_EXE, args, cb)
|
||||
}
|
||||
|
||||
// Create desktop/start menu shortcuts using the Squirrel Update.exe command line API
|
||||
/**
|
||||
* Create desktop and start menu shortcuts using the Squirrel `Update.exe`
|
||||
* command.
|
||||
*/
|
||||
function createShortcuts (cb) {
|
||||
spawnUpdate(['--createShortcut', exeName], cb)
|
||||
spawnUpdate(['--createShortcut', EXE_NAME], cb)
|
||||
}
|
||||
|
||||
// Update desktop/start menu shortcuts using the Squirrel Update.exe command line API
|
||||
/**
|
||||
* Update desktop and start menu shortcuts using the Squirrel `Update.exe`
|
||||
* command.
|
||||
*/
|
||||
function updateShortcuts (cb) {
|
||||
var homeDir = os.homedir()
|
||||
if (homeDir) {
|
||||
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
||||
// Check if the desktop shortcut has been previously deleted and and keep it deleted
|
||||
// if it was
|
||||
// If the desktop shortcut was deleted by the user, then keep it deleted.
|
||||
fs.access(desktopShortcutPath, function (err) {
|
||||
var desktopShortcutExists = !err
|
||||
createShortcuts(function () {
|
||||
@@ -133,7 +142,10 @@ function updateShortcuts (cb) {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API
|
||||
/**
|
||||
* Remove desktop and start menu shortcuts using the Squirrel `Update.exe`
|
||||
* command.
|
||||
*/
|
||||
function removeShortcuts (cb) {
|
||||
spawnUpdate(['--removeShortcut', exeName], cb)
|
||||
spawnUpdate(['--removeShortcut', EXE_NAME], cb)
|
||||
}
|
||||
|
||||
35
main/thumbnail.js
Normal file
35
main/thumbnail.js
Normal file
@@ -0,0 +1,35 @@
|
||||
module.exports = {
|
||||
showPlayerThumbnailBar,
|
||||
hidePlayerThumbnailBar,
|
||||
updateThumbarButtons
|
||||
}
|
||||
|
||||
var path = require('path')
|
||||
var config = require('../config')
|
||||
|
||||
var windows = require('./windows')
|
||||
|
||||
// gets called on player open
|
||||
function showPlayerThumbnailBar () {
|
||||
updateThumbarButtons(false)
|
||||
}
|
||||
|
||||
// gets called on player close
|
||||
function hidePlayerThumbnailBar () {
|
||||
windows.main.win.setThumbarButtons([])
|
||||
}
|
||||
|
||||
function updateThumbarButtons (isPaused) {
|
||||
var icon = isPaused ? 'PlayThumbnailBarButton.png' : 'PauseThumbnailBarButton.png'
|
||||
var tooltip = isPaused ? 'Play' : 'Pause'
|
||||
var buttons = [
|
||||
{
|
||||
tooltip: tooltip,
|
||||
icon: path.join(config.STATIC_PATH, icon),
|
||||
click: function () {
|
||||
windows.main.send('dispatch', 'playPause')
|
||||
}
|
||||
}
|
||||
]
|
||||
windows.main.win.setThumbarButtons(buttons)
|
||||
}
|
||||
118
main/tray.js
118
main/tray.js
@@ -1,51 +1,62 @@
|
||||
module.exports = {
|
||||
hasTray,
|
||||
init,
|
||||
hasTray
|
||||
onWindowBlur,
|
||||
onWindowFocus
|
||||
}
|
||||
|
||||
var cp = require('child_process')
|
||||
var path = require('path')
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
|
||||
var config = require('../config')
|
||||
var windows = require('./windows')
|
||||
|
||||
var trayIcon
|
||||
var tray
|
||||
|
||||
function init () {
|
||||
// OS X has no tray icon
|
||||
if (process.platform === 'darwin') return
|
||||
|
||||
// On Linux, asynchronously check for libappindicator1
|
||||
if (process.platform === 'linux') {
|
||||
checkLinuxTraySupport(function (supportsTray) {
|
||||
if (supportsTray) createTrayIcon()
|
||||
})
|
||||
initLinux()
|
||||
}
|
||||
|
||||
// Windows always supports minimize-to-tray
|
||||
if (process.platform === 'win32') createTrayIcon()
|
||||
if (process.platform === 'win32') {
|
||||
initWin32()
|
||||
}
|
||||
// OS X apps generally do not have menu bar icons
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there a tray icon is active.
|
||||
*/
|
||||
function hasTray () {
|
||||
return !!trayIcon
|
||||
return !!tray
|
||||
}
|
||||
|
||||
function createTrayIcon () {
|
||||
trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
|
||||
|
||||
// On Windows, left click to open the app, right click for context menu
|
||||
// On Linux, any click (right or left) opens the context menu
|
||||
trayIcon.on('click', showApp)
|
||||
|
||||
// Show the tray context menu, and keep the available commands up to date
|
||||
function onWindowBlur () {
|
||||
if (!tray) return
|
||||
updateTrayMenu()
|
||||
windows.main.on('show', updateTrayMenu)
|
||||
windows.main.on('hide', updateTrayMenu)
|
||||
}
|
||||
|
||||
function onWindowFocus () {
|
||||
if (!tray) return
|
||||
updateTrayMenu()
|
||||
}
|
||||
|
||||
function initLinux () {
|
||||
checkLinuxTraySupport(function (supportsTray) {
|
||||
if (supportsTray) createTray()
|
||||
})
|
||||
}
|
||||
|
||||
function initWin32 () {
|
||||
createTray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for libappindicator1 support before creating tray icon
|
||||
*/
|
||||
function checkLinuxTraySupport (cb) {
|
||||
var cp = require('child_process')
|
||||
|
||||
// Check that we're on Ubuntu (or another debian system) and that we have
|
||||
// libappindicator1. If WebTorrent was installed from the deb file, we should
|
||||
// always have it. If it was installed from the zip file, we might not.
|
||||
@@ -57,25 +68,48 @@ function checkLinuxTraySupport (cb) {
|
||||
})
|
||||
}
|
||||
|
||||
function createTray () {
|
||||
tray = new electron.Tray(getIconPath())
|
||||
|
||||
// On Windows, left click opens the app, right click opens the context menu.
|
||||
// On Linux, any click (left or right) opens the context menu.
|
||||
tray.on('click', () => windows.main.show())
|
||||
|
||||
// Show the tray context menu, and keep the available commands up to date
|
||||
updateTrayMenu()
|
||||
}
|
||||
|
||||
function updateTrayMenu () {
|
||||
var showHideMenuItem
|
||||
if (windows.main.isVisible()) {
|
||||
showHideMenuItem = { label: 'Hide to tray', click: hideApp }
|
||||
} else {
|
||||
showHideMenuItem = { label: 'Show', click: showApp }
|
||||
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
tray.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
function getMenuTemplate () {
|
||||
return [
|
||||
getToggleItem(),
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => app.quit()
|
||||
}
|
||||
]
|
||||
|
||||
function getToggleItem () {
|
||||
if (windows.main.win.isVisible()) {
|
||||
return {
|
||||
label: 'Hide to tray',
|
||||
click: () => windows.main.hide()
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
label: 'Show WebTorrent',
|
||||
click: () => windows.main.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
var contextMenu = electron.Menu.buildFromTemplate([
|
||||
showHideMenuItem,
|
||||
{ label: 'Quit', click: () => app.quit() }
|
||||
])
|
||||
trayIcon.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
function showApp () {
|
||||
windows.main.show()
|
||||
}
|
||||
|
||||
function hideApp () {
|
||||
windows.main.hide()
|
||||
windows.main.send('dispatch', 'backToList')
|
||||
function getIconPath () {
|
||||
return process.platform === 'win32'
|
||||
? config.APP_ICON + '.ico'
|
||||
: config.APP_ICON + '.png'
|
||||
}
|
||||
|
||||
@@ -21,27 +21,27 @@ function init () {
|
||||
}
|
||||
}
|
||||
|
||||
// The Electron auto-updater does not support Linux yet, so manually check for updates and
|
||||
// `show the user a modal notification.
|
||||
// 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 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.dispatch('updateAvailable', data.version)
|
||||
} else if (res.statusCode === 204) {
|
||||
// No update available
|
||||
} else {
|
||||
// Unexpected status code
|
||||
log(`Update error: Unexpected status code: ${res.statusCode}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
139
main/windows.js
139
main/windows.js
@@ -1,139 +0,0 @@
|
||||
var windows = module.exports = {
|
||||
about: null,
|
||||
main: null,
|
||||
createAboutWindow,
|
||||
createWebTorrentHiddenWindow,
|
||||
createMainWindow,
|
||||
focusWindow
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
|
||||
var config = require('../config')
|
||||
var menu = require('./menu')
|
||||
var tray = require('./tray')
|
||||
|
||||
function createAboutWindow () {
|
||||
if (windows.about) {
|
||||
return focusWindow(windows.about)
|
||||
}
|
||||
var win = windows.about = new electron.BrowserWindow({
|
||||
backgroundColor: '#ECECEC',
|
||||
show: false,
|
||||
center: true,
|
||||
resizable: false,
|
||||
icon: config.APP_ICON + '.png',
|
||||
title: process.platform !== 'darwin'
|
||||
? 'About ' + config.APP_WINDOW_TITLE
|
||||
: '',
|
||||
useContentSize: true, // Specify web page size without OS chrome
|
||||
width: 300,
|
||||
height: 170,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
fullscreen: false,
|
||||
skipTaskbar: true
|
||||
})
|
||||
win.loadURL(config.WINDOW_ABOUT)
|
||||
|
||||
// No window menu
|
||||
win.setMenu(null)
|
||||
|
||||
win.webContents.on('did-finish-load', function () {
|
||||
win.show()
|
||||
})
|
||||
|
||||
win.once('closed', function () {
|
||||
windows.about = null
|
||||
})
|
||||
}
|
||||
|
||||
function createWebTorrentHiddenWindow () {
|
||||
var win = windows.webtorrent = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
show: false,
|
||||
center: true,
|
||||
title: 'webtorrent-hidden-window',
|
||||
useContentSize: true,
|
||||
width: 150,
|
||||
height: 150,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
resizable: false,
|
||||
fullscreenable: false,
|
||||
fullscreen: false,
|
||||
skipTaskbar: true
|
||||
})
|
||||
win.loadURL(config.WINDOW_WEBTORRENT)
|
||||
|
||||
// Prevent killing the WebTorrent process
|
||||
win.on('close', function (e) {
|
||||
if (!app.isQuitting) {
|
||||
e.preventDefault()
|
||||
win.hide()
|
||||
}
|
||||
})
|
||||
|
||||
win.once('closed', function () {
|
||||
windows.webtorrent = null
|
||||
})
|
||||
}
|
||||
|
||||
var HEADER_HEIGHT = 37
|
||||
var TORRENT_HEIGHT = 100
|
||||
|
||||
function createMainWindow () {
|
||||
if (windows.main) {
|
||||
return focusWindow(windows.main)
|
||||
}
|
||||
var win = windows.main = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
darkTheme: true, // Forces dark theme (GTK+3)
|
||||
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
|
||||
minWidth: config.WINDOW_MIN_WIDTH,
|
||||
minHeight: config.WINDOW_MIN_HEIGHT,
|
||||
show: false, // Hide window until renderer sends 'ipcReady' event
|
||||
title: config.APP_WINDOW_TITLE,
|
||||
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
||||
useContentSize: true, // Specify web page size without OS chrome
|
||||
width: 500,
|
||||
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
||||
})
|
||||
win.loadURL(config.WINDOW_MAIN)
|
||||
if (process.platform === 'darwin') {
|
||||
win.setSheetOffset(HEADER_HEIGHT)
|
||||
}
|
||||
|
||||
win.webContents.on('dom-ready', function () {
|
||||
menu.onToggleFullScreen()
|
||||
})
|
||||
|
||||
win.on('blur', menu.onWindowHide)
|
||||
win.on('focus', menu.onWindowShow)
|
||||
|
||||
win.on('enter-full-screen', () => menu.onToggleFullScreen(true))
|
||||
win.on('leave-full-screen', () => menu.onToggleFullScreen(false))
|
||||
|
||||
win.on('close', function (e) {
|
||||
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
||||
app.quit()
|
||||
} else if (!app.isQuitting) {
|
||||
e.preventDefault()
|
||||
win.hide()
|
||||
win.send('dispatch', 'backToList')
|
||||
}
|
||||
})
|
||||
|
||||
win.once('closed', function () {
|
||||
windows.main = null
|
||||
})
|
||||
}
|
||||
|
||||
function focusWindow (win) {
|
||||
if (win.isMinimized()) {
|
||||
win.restore()
|
||||
}
|
||||
win.show() // shows and gives focus
|
||||
}
|
||||
48
main/windows/about.js
Normal file
48
main/windows/about.js
Normal file
@@ -0,0 +1,48 @@
|
||||
var about = module.exports = {
|
||||
init,
|
||||
win: null
|
||||
}
|
||||
|
||||
var config = require('../../config')
|
||||
var electron = require('electron')
|
||||
|
||||
function init () {
|
||||
if (about.win) {
|
||||
return about.win.show()
|
||||
}
|
||||
|
||||
var win = about.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#ECECEC',
|
||||
center: true,
|
||||
fullscreen: false,
|
||||
height: 170,
|
||||
icon: getIconPath(),
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
resizable: false,
|
||||
show: false,
|
||||
skipTaskbar: true,
|
||||
title: 'About ' + config.APP_WINDOW_TITLE,
|
||||
useContentSize: true,
|
||||
width: 300
|
||||
})
|
||||
|
||||
win.loadURL(config.WINDOW_ABOUT)
|
||||
|
||||
// No menu on the About window
|
||||
win.setMenu(null)
|
||||
|
||||
win.webContents.once('did-finish-load', function () {
|
||||
win.show()
|
||||
})
|
||||
|
||||
win.once('closed', function () {
|
||||
about.win = null
|
||||
})
|
||||
}
|
||||
|
||||
function getIconPath () {
|
||||
return process.platform === 'win32'
|
||||
? config.APP_ICON + '.ico'
|
||||
: config.APP_ICON + '.png'
|
||||
}
|
||||
3
main/windows/index.js
Normal file
3
main/windows/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
exports.about = require('./about')
|
||||
exports.main = require('./main')
|
||||
exports.webtorrent = require('./webtorrent')
|
||||
220
main/windows/main.js
Normal file
220
main/windows/main.js
Normal file
@@ -0,0 +1,220 @@
|
||||
var main = module.exports = {
|
||||
dispatch,
|
||||
hide,
|
||||
init,
|
||||
send,
|
||||
setAspectRatio,
|
||||
setBounds,
|
||||
setProgress,
|
||||
setTitle,
|
||||
show,
|
||||
toggleAlwaysOnTop,
|
||||
toggleDevTools,
|
||||
toggleFullScreen,
|
||||
win: null
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
|
||||
var config = require('../../config')
|
||||
var log = require('../log')
|
||||
var menu = require('../menu')
|
||||
var tray = require('../tray')
|
||||
|
||||
var HEADER_HEIGHT = 37
|
||||
var TORRENT_HEIGHT = 100
|
||||
|
||||
function init () {
|
||||
if (main.win) {
|
||||
return main.win.show()
|
||||
}
|
||||
var win = main.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
darkTheme: true, // Forces dark theme (GTK+3)
|
||||
icon: getIconPath(), // Window icon (Windows, Linux)
|
||||
minWidth: config.WINDOW_MIN_WIDTH,
|
||||
minHeight: config.WINDOW_MIN_HEIGHT,
|
||||
title: config.APP_WINDOW_TITLE,
|
||||
titleBarStyle: 'hidden-inset', // Hide title bar (OS X)
|
||||
useContentSize: true, // Specify web page size without OS chrome
|
||||
width: 500,
|
||||
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
||||
})
|
||||
|
||||
win.loadURL(config.WINDOW_MAIN)
|
||||
|
||||
if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT)
|
||||
|
||||
win.webContents.on('dom-ready', function () {
|
||||
menu.onToggleFullScreen(main.win.isFullScreen())
|
||||
})
|
||||
|
||||
win.on('blur', onWindowBlur)
|
||||
win.on('focus', onWindowFocus)
|
||||
|
||||
win.on('hide', onWindowBlur)
|
||||
win.on('show', onWindowFocus)
|
||||
|
||||
win.on('enter-full-screen', function () {
|
||||
menu.onToggleFullScreen(true)
|
||||
send('fullscreenChanged', true)
|
||||
win.setMenuBarVisibility(false)
|
||||
})
|
||||
|
||||
win.on('leave-full-screen', function () {
|
||||
menu.onToggleFullScreen(false)
|
||||
send('fullscreenChanged', false)
|
||||
win.setMenuBarVisibility(true)
|
||||
})
|
||||
|
||||
win.on('close', function (e) {
|
||||
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
||||
app.quit()
|
||||
} else if (!app.isQuitting) {
|
||||
e.preventDefault()
|
||||
hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function dispatch (...args) {
|
||||
send('dispatch', ...args)
|
||||
}
|
||||
|
||||
function hide () {
|
||||
if (!main.win) return
|
||||
main.win.send('dispatch', 'backToList')
|
||||
main.win.hide()
|
||||
}
|
||||
|
||||
function send (...args) {
|
||||
if (!main.win) return
|
||||
main.win.send(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce window aspect ratio. Remove with 0. (OS X)
|
||||
*/
|
||||
function setAspectRatio (aspectRatio) {
|
||||
if (!main.win) return
|
||||
main.win.setAspectRatio(aspectRatio)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the size of the window.
|
||||
* TODO: Clean this up? Seems overly complicated.
|
||||
*/
|
||||
function setBounds (bounds, maximize) {
|
||||
// Do nothing in fullscreen
|
||||
if (!main.win || main.win.isFullScreen()) {
|
||||
log('setBounds: not setting bounds because we\'re in full screen')
|
||||
return
|
||||
}
|
||||
|
||||
// Maximize or minimize, if the second argument is present
|
||||
var willBeMaximized
|
||||
if (maximize === true) {
|
||||
if (!main.win.isMaximized()) {
|
||||
log('setBounds: maximizing')
|
||||
main.win.maximize()
|
||||
}
|
||||
willBeMaximized = true
|
||||
} else if (maximize === false) {
|
||||
if (main.win.isMaximized()) {
|
||||
log('setBounds: unmaximizing')
|
||||
main.win.unmaximize()
|
||||
}
|
||||
willBeMaximized = false
|
||||
} else {
|
||||
willBeMaximized = main.win.isMaximized()
|
||||
}
|
||||
|
||||
// Assuming we're not maximized or maximizing, set the window size
|
||||
if (!willBeMaximized) {
|
||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
||||
if (bounds.x === null && bounds.y === null) {
|
||||
// X and Y not specified? By default, center on current screen
|
||||
var scr = electron.screen.getDisplayMatching(main.win.getBounds())
|
||||
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||
}
|
||||
main.win.setBounds(bounds, true)
|
||||
} else {
|
||||
log('setBounds: not setting bounds because of window maximization')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set progress bar to [0, 1]. Indeterminate when > 1. Remove with < 0.
|
||||
*/
|
||||
function setProgress (progress) {
|
||||
if (!main.win) return
|
||||
main.win.setProgressBar(progress)
|
||||
}
|
||||
|
||||
function setTitle (title) {
|
||||
if (!main.win) return
|
||||
main.win.setTitle(title)
|
||||
}
|
||||
|
||||
function show () {
|
||||
if (!main.win) return
|
||||
main.win.show()
|
||||
}
|
||||
|
||||
// Sets whether the window should always show on top of other windows
|
||||
function toggleAlwaysOnTop (flag) {
|
||||
if (!main.win) return
|
||||
if (flag == null) {
|
||||
flag = !main.win.isAlwaysOnTop()
|
||||
}
|
||||
log(`toggleAlwaysOnTop ${flag}`)
|
||||
main.win.setAlwaysOnTop(flag)
|
||||
menu.onToggleAlwaysOnTop(flag)
|
||||
}
|
||||
|
||||
function toggleDevTools () {
|
||||
if (!main.win) return
|
||||
log('toggleDevTools')
|
||||
if (main.win.webContents.isDevToolsOpened()) {
|
||||
main.win.webContents.closeDevTools()
|
||||
} else {
|
||||
main.win.webContents.openDevTools({ detach: true })
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullScreen (flag) {
|
||||
if (!main.win || !main.win.isVisible()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (flag == null) flag = !main.win.isFullScreen()
|
||||
|
||||
log(`toggleFullScreen ${flag}`)
|
||||
|
||||
if (flag) {
|
||||
// Fullscreen and aspect ratio do not play well together. (OS X)
|
||||
main.win.setAspectRatio(0)
|
||||
}
|
||||
|
||||
main.win.setFullScreen(flag)
|
||||
}
|
||||
|
||||
function onWindowBlur () {
|
||||
menu.onWindowBlur()
|
||||
tray.onWindowBlur()
|
||||
}
|
||||
|
||||
function onWindowFocus () {
|
||||
menu.onWindowFocus()
|
||||
tray.onWindowFocus()
|
||||
}
|
||||
|
||||
function getIconPath () {
|
||||
return process.platform === 'win32'
|
||||
? config.APP_ICON + '.ico'
|
||||
: config.APP_ICON + '.png'
|
||||
}
|
||||
62
main/windows/webtorrent.js
Normal file
62
main/windows/webtorrent.js
Normal file
@@ -0,0 +1,62 @@
|
||||
var webtorrent = module.exports = {
|
||||
init,
|
||||
send,
|
||||
show,
|
||||
toggleDevTools,
|
||||
win: null
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var config = require('../../config')
|
||||
var log = require('../log')
|
||||
|
||||
function init () {
|
||||
var win = webtorrent.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
center: true,
|
||||
fullscreen: false,
|
||||
fullscreenable: false,
|
||||
height: 150,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
resizable: false,
|
||||
show: false,
|
||||
skipTaskbar: true,
|
||||
title: 'webtorrent-hidden-window',
|
||||
useContentSize: true,
|
||||
width: 150
|
||||
})
|
||||
|
||||
win.loadURL(config.WINDOW_WEBTORRENT)
|
||||
|
||||
// Prevent killing the WebTorrent process
|
||||
win.on('close', function (e) {
|
||||
if (electron.app.isQuitting) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
win.hide()
|
||||
})
|
||||
}
|
||||
|
||||
function show () {
|
||||
if (!webtorrent.win) return
|
||||
webtorrent.win.show()
|
||||
}
|
||||
|
||||
function send (...args) {
|
||||
if (!webtorrent.win) return
|
||||
webtorrent.win.send(...args)
|
||||
}
|
||||
|
||||
function toggleDevTools () {
|
||||
if (!webtorrent.win) return
|
||||
log('toggleDevTools')
|
||||
if (webtorrent.win.webContents.isDevToolsOpened()) {
|
||||
webtorrent.win.webContents.closeDevTools()
|
||||
webtorrent.win.hide()
|
||||
} else {
|
||||
webtorrent.win.webContents.openDevTools({ detach: true })
|
||||
}
|
||||
}
|
||||
14
package.json
14
package.json
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "webtorrent-desktop",
|
||||
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
||||
"version": "0.5.1",
|
||||
"version": "0.8.0",
|
||||
"author": {
|
||||
"name": "WebTorrent, LLC",
|
||||
"email": "feross@feross.org",
|
||||
"email": "feross@webtorrent.io",
|
||||
"url": "https://webtorrent.io"
|
||||
},
|
||||
"bin": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "https://github.com/feross/webtorrent-desktop/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"airplay-js": "guerrerocarlos/node-airplay-js",
|
||||
"airplayer": "^2.0.0",
|
||||
"application-config": "^0.2.1",
|
||||
"bitfield": "^1.0.2",
|
||||
"chromecasts": "^1.8.0",
|
||||
@@ -22,8 +22,7 @@
|
||||
"deep-equal": "^1.0.1",
|
||||
"dlnacasts": "^0.1.0",
|
||||
"drag-drop": "^2.11.0",
|
||||
"electron-localshortcut": "^0.6.0",
|
||||
"electron-prebuilt": "1.0.2",
|
||||
"electron-prebuilt": "1.2.1",
|
||||
"fs-extra": "^0.27.0",
|
||||
"hyperx": "^2.0.2",
|
||||
"iso-639-1": "^1.2.1",
|
||||
@@ -31,15 +30,18 @@
|
||||
"main-loop": "^3.2.0",
|
||||
"musicmetadata": "^2.0.2",
|
||||
"network-address": "^1.1.0",
|
||||
"parse-torrent": "^5.7.3",
|
||||
"prettier-bytes": "^1.0.1",
|
||||
"run-parallel": "^1.1.6",
|
||||
"semver": "^5.1.0",
|
||||
"simple-concat": "^1.0.0",
|
||||
"simple-get": "^2.0.0",
|
||||
"srt-to-vtt": "^1.1.1",
|
||||
"virtual-dom": "^2.1.1",
|
||||
"vlc-command": "^1.0.1",
|
||||
"webtorrent": "0.x",
|
||||
"winreg": "^1.2.0"
|
||||
"winreg": "^1.2.0",
|
||||
"zero-fill": "^2.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-zip": "^2.0.1",
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
// * Starts and stops casting, provides remote video controls
|
||||
module.exports = {
|
||||
init,
|
||||
open,
|
||||
close,
|
||||
toggleMenu,
|
||||
selectDevice,
|
||||
stop,
|
||||
play,
|
||||
pause,
|
||||
seek,
|
||||
setVolume
|
||||
setVolume,
|
||||
setRate
|
||||
}
|
||||
|
||||
var airplay = require('airplay-js')
|
||||
var airplayer = require('airplayer')()
|
||||
var chromecasts = require('chromecasts')()
|
||||
var dlnacasts = require('dlnacasts')()
|
||||
|
||||
@@ -31,25 +33,49 @@ function init (appState, callback) {
|
||||
state = appState
|
||||
update = callback
|
||||
|
||||
state.devices.chromecast = chromecastPlayer()
|
||||
state.devices.dlna = dlnaPlayer()
|
||||
state.devices.airplay = airplayPlayer()
|
||||
|
||||
// Listen for devices: Chromecast, DLNA and Airplay
|
||||
chromecasts.on('update', function (player) {
|
||||
state.devices.chromecast = chromecastPlayer(player)
|
||||
chromecasts.on('update', function (device) {
|
||||
// TODO: how do we tell if there are *no longer* any Chromecasts available?
|
||||
// From looking at the code, chromecasts.players only grows, never shrinks
|
||||
state.devices.chromecast.addDevice(device)
|
||||
})
|
||||
|
||||
dlnacasts.on('update', function (player) {
|
||||
state.devices.dlna = dlnaPlayer(player)
|
||||
dlnacasts.on('update', function (device) {
|
||||
state.devices.dlna.addDevice(device)
|
||||
})
|
||||
|
||||
var browser = airplay.createBrowser()
|
||||
browser.on('deviceOn', function (player) {
|
||||
state.devices.airplay = airplayPlayer(player)
|
||||
}).start()
|
||||
airplayer.on('update', function (device) {
|
||||
state.devices.airplay.addDevice(device)
|
||||
})
|
||||
}
|
||||
|
||||
// chromecast player implementation
|
||||
function chromecastPlayer (player) {
|
||||
function addEvents () {
|
||||
player.on('error', function (err) {
|
||||
function chromecastPlayer () {
|
||||
var ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
open,
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
status,
|
||||
seek,
|
||||
volume
|
||||
}
|
||||
return ret
|
||||
|
||||
function getDevices () {
|
||||
return chromecasts.players
|
||||
}
|
||||
|
||||
function addDevice (device) {
|
||||
device.on('error', function (err) {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
@@ -57,7 +83,8 @@ function chromecastPlayer (player) {
|
||||
})
|
||||
update()
|
||||
})
|
||||
player.on('disconnect', function () {
|
||||
device.on('disconnect', function () {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
update()
|
||||
})
|
||||
@@ -65,7 +92,7 @@ function chromecastPlayer (player) {
|
||||
|
||||
function open () {
|
||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||
player.play(state.server.networkURL, {
|
||||
ret.device.play(state.server.networkURL, {
|
||||
type: 'video/mp4',
|
||||
title: config.APP_NAME + ' - ' + torrentSummary.name
|
||||
}, function (err) {
|
||||
@@ -83,19 +110,19 @@ function chromecastPlayer (player) {
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
player.play(null, null, callback)
|
||||
ret.device.play(null, null, callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
player.pause(callback)
|
||||
ret.device.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
player.stop(callback)
|
||||
ret.device.stop(callback)
|
||||
}
|
||||
|
||||
function status () {
|
||||
player.status(function (err, status) {
|
||||
ret.device.status(function (err, status) {
|
||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||
state.playing.currentTime = status.currentTime
|
||||
@@ -105,36 +132,59 @@ function chromecastPlayer (player) {
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
player.seek(time, callback)
|
||||
ret.device.seek(time, callback)
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
player.volume(volume, callback)
|
||||
}
|
||||
|
||||
addEvents()
|
||||
|
||||
return {
|
||||
player: player,
|
||||
open: open,
|
||||
play: play,
|
||||
pause: pause,
|
||||
stop: stop,
|
||||
status: status,
|
||||
seek: seek,
|
||||
volume: volume
|
||||
ret.device.volume(volume, callback)
|
||||
}
|
||||
}
|
||||
|
||||
// airplay player implementation
|
||||
function airplayPlayer (player) {
|
||||
function airplayPlayer () {
|
||||
var ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
open,
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
status,
|
||||
seek,
|
||||
volume
|
||||
}
|
||||
return ret
|
||||
|
||||
function addDevice (player) {
|
||||
player.on('event', function (event) {
|
||||
switch (event.state) {
|
||||
case 'loading':
|
||||
break
|
||||
case 'playing':
|
||||
state.playing.isPaused = false
|
||||
break
|
||||
case 'paused':
|
||||
state.playing.isPaused = true
|
||||
break
|
||||
case 'stopped':
|
||||
break
|
||||
}
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function getDevices () {
|
||||
return airplayer.players
|
||||
}
|
||||
|
||||
function open () {
|
||||
player.play(state.server.networkURL, 0, function (res) {
|
||||
if (res.statusCode !== 200) {
|
||||
ret.device.play(state.server.networkURL, function (err, res) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to AirPlay.'
|
||||
message: 'Could not connect to AirPlay. ' + err.message
|
||||
})
|
||||
} else {
|
||||
state.playing.location = 'airplay'
|
||||
@@ -144,55 +194,67 @@ function airplayPlayer (player) {
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
player.rate(1, callback)
|
||||
ret.device.resume(callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
player.rate(0, callback)
|
||||
ret.device.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
player.stop(callback)
|
||||
ret.device.stop(callback)
|
||||
}
|
||||
|
||||
function status () {
|
||||
player.status(function (status) {
|
||||
state.playing.isPaused = status.rate === 0
|
||||
state.playing.currentTime = status.position
|
||||
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume
|
||||
// According to docs is in [-30 - 0] (db) range
|
||||
// should be converted to [0 - 1] using (val / 30 + 1)
|
||||
update()
|
||||
ret.device.playbackInfo(function (err, res, status) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to AirPlay. ' + err.message
|
||||
})
|
||||
} else {
|
||||
state.playing.isPaused = status.rate === 0
|
||||
state.playing.currentTime = status.position
|
||||
update()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
player.scrub(time, callback)
|
||||
ret.device.scrub(time, callback)
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
// TODO remove line below once we can fetch the information in status update
|
||||
// AirPlay doesn't support volume
|
||||
// TODO: We should just disable the volume slider
|
||||
state.playing.volume = volume
|
||||
volume = (volume - 1) * 30
|
||||
player.volume(volume, callback)
|
||||
}
|
||||
|
||||
return {
|
||||
player: player,
|
||||
open: open,
|
||||
play: play,
|
||||
pause: pause,
|
||||
stop: stop,
|
||||
status: status,
|
||||
seek: seek,
|
||||
volume: volume
|
||||
}
|
||||
}
|
||||
|
||||
// DLNA player implementation
|
||||
function dlnaPlayer (player) {
|
||||
function addEvents () {
|
||||
player.on('error', function (err) {
|
||||
var ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
open,
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
status,
|
||||
seek,
|
||||
volume
|
||||
}
|
||||
return ret
|
||||
|
||||
function getDevices () {
|
||||
return dlnacasts.players
|
||||
}
|
||||
|
||||
function addDevice (device) {
|
||||
device.on('error', function (err) {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
@@ -200,7 +262,8 @@ function dlnaPlayer (player) {
|
||||
})
|
||||
update()
|
||||
})
|
||||
player.on('disconnect', function () {
|
||||
device.on('disconnect', function () {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
update()
|
||||
})
|
||||
@@ -208,7 +271,7 @@ function dlnaPlayer (player) {
|
||||
|
||||
function open () {
|
||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||
player.play(state.server.networkURL, {
|
||||
ret.device.play(state.server.networkURL, {
|
||||
type: 'video/mp4',
|
||||
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
||||
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
||||
@@ -227,19 +290,19 @@ function dlnaPlayer (player) {
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
player.play(null, null, callback)
|
||||
ret.device.play(null, null, callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
player.pause(callback)
|
||||
ret.device.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
player.stop(callback)
|
||||
ret.device.stop(callback)
|
||||
}
|
||||
|
||||
function status () {
|
||||
player.status(function (err, status) {
|
||||
ret.device.status(function (err, status) {
|
||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||
state.playing.currentTime = status.currentTime
|
||||
@@ -249,61 +312,78 @@ function dlnaPlayer (player) {
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
player.seek(time, callback)
|
||||
ret.device.seek(time, callback)
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
player.volume(volume, function (err) {
|
||||
ret.device.volume(volume, function (err) {
|
||||
// quick volume update
|
||||
state.playing.volume = volume
|
||||
callback(err)
|
||||
})
|
||||
}
|
||||
|
||||
addEvents()
|
||||
|
||||
return {
|
||||
player: player,
|
||||
open: open,
|
||||
play: play,
|
||||
pause: pause,
|
||||
stop: stop,
|
||||
status: status,
|
||||
seek: seek,
|
||||
volume: volume
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling cast device state, whenever we're connected
|
||||
function startStatusInterval () {
|
||||
statusInterval = setInterval(function () {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.status()
|
||||
}
|
||||
var player = getPlayer()
|
||||
if (player) player.status()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function open (location) {
|
||||
/*
|
||||
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
|
||||
* The menu lists eg. all Chromecasts detected; the user can click one to cast.
|
||||
* If the menu was already showing for that type, hides the menu.
|
||||
*/
|
||||
function toggleMenu (location) {
|
||||
// If the menu is already showing, hide it
|
||||
if (state.devices.castMenu && state.devices.castMenu.location === location) {
|
||||
state.devices.castMenu = null
|
||||
return
|
||||
}
|
||||
|
||||
// Never cast to two devices at the same time
|
||||
if (state.playing.location !== 'local') {
|
||||
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
|
||||
}
|
||||
|
||||
state.playing.location = location + '-pending'
|
||||
var device = getDevice(location)
|
||||
if (device) {
|
||||
getDevice(location).open()
|
||||
startStatusInterval()
|
||||
}
|
||||
// Find all cast devices of the given type
|
||||
var player = getPlayer(location)
|
||||
var devices = player ? player.getDevices() : []
|
||||
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
||||
|
||||
// Show a menu
|
||||
state.devices.castMenu = {location, devices}
|
||||
}
|
||||
|
||||
function selectDevice (index) {
|
||||
var {location, devices} = state.devices.castMenu
|
||||
|
||||
// Start casting
|
||||
var player = getPlayer(location)
|
||||
player.device = devices[index]
|
||||
player.open()
|
||||
|
||||
// Poll the casting device's status every few seconds
|
||||
startStatusInterval()
|
||||
|
||||
// Show the Connecting... screen
|
||||
state.devices.castMenu = null
|
||||
state.playing.castName = devices[index].name
|
||||
state.playing.location = location + '-pending'
|
||||
update()
|
||||
}
|
||||
|
||||
// Stops casting, move video back to local screen
|
||||
function close () {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.stop(stoppedCasting)
|
||||
function stop () {
|
||||
var player = getPlayer()
|
||||
if (player) {
|
||||
player.stop(function () {
|
||||
player.device = null
|
||||
stoppedCasting()
|
||||
})
|
||||
clearInterval(statusInterval)
|
||||
} else {
|
||||
stoppedCasting()
|
||||
@@ -316,8 +396,8 @@ function stoppedCasting () {
|
||||
update()
|
||||
}
|
||||
|
||||
function getDevice (location) {
|
||||
if (location && state.devices[location]) {
|
||||
function getPlayer (location) {
|
||||
if (location) {
|
||||
return state.devices[location]
|
||||
} else if (state.playing.location === 'chromecast') {
|
||||
return state.devices.chromecast
|
||||
@@ -331,31 +411,39 @@ function getDevice (location) {
|
||||
}
|
||||
|
||||
function play () {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.play(castCallback)
|
||||
}
|
||||
var player = getPlayer()
|
||||
if (player) player.play(castCallback)
|
||||
}
|
||||
|
||||
function pause () {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.pause(castCallback)
|
||||
var player = getPlayer()
|
||||
if (player) player.pause(castCallback)
|
||||
}
|
||||
|
||||
function setRate (rate) {
|
||||
var player
|
||||
var result = true
|
||||
if (state.playing.location === 'chromecast') {
|
||||
// TODO find how to control playback rate on chromecast
|
||||
castCallback()
|
||||
result = false
|
||||
} else if (state.playing.location === 'airplay') {
|
||||
player = state.devices.airplay
|
||||
player.rate(rate, castCallback)
|
||||
} else {
|
||||
result = false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function seek (time) {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.seek(time, castCallback)
|
||||
}
|
||||
var player = getPlayer()
|
||||
if (player) player.seek(time, castCallback)
|
||||
}
|
||||
|
||||
function setVolume (volume) {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
device.volume(volume, castCallback)
|
||||
}
|
||||
var player = getPlayer()
|
||||
if (player) player.volume(volume, castCallback)
|
||||
}
|
||||
|
||||
function castCallback () {
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
module.exports = {
|
||||
setDispatch,
|
||||
dispatch,
|
||||
dispatcher
|
||||
dispatcher,
|
||||
setDispatch
|
||||
}
|
||||
|
||||
// Memoize most of our event handlers, which are functions in the form
|
||||
// () => dispatch(<args>)
|
||||
// ... this prevents virtual-dom from updating every listener on every update()
|
||||
var _dispatchers = {}
|
||||
var _dispatch = () => {}
|
||||
var dispatchers = {}
|
||||
var _dispatch = function () {}
|
||||
|
||||
function setDispatch (dispatch) {
|
||||
_dispatch = dispatch
|
||||
}
|
||||
|
||||
// Get a _memoized event handler that calls dispatch()
|
||||
// All args must be JSON-able
|
||||
function dispatch (...args) {
|
||||
_dispatch(...args)
|
||||
}
|
||||
|
||||
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
|
||||
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler
|
||||
// function. This prevents virtual-dom from updating the listener functions on
|
||||
// each update().
|
||||
function dispatcher (...args) {
|
||||
var json = JSON.stringify(args)
|
||||
var handler = _dispatchers[json]
|
||||
var str = JSON.stringify(args)
|
||||
var handler = dispatchers[str]
|
||||
if (!handler) {
|
||||
handler = _dispatchers[json] = (e) => {
|
||||
// Don't click on whatever is below the button
|
||||
handler = dispatchers[str] = function (e) {
|
||||
// Do not propagate click to elements below the button
|
||||
e.stopPropagation()
|
||||
// Don't regisiter clicks on disabled buttons
|
||||
if (e.currentTarget.classList.contains('disabled')) return
|
||||
_dispatch.apply(null, args)
|
||||
|
||||
if (e.currentTarget.classList.contains('disabled')) {
|
||||
// Ignore clicks on disabled elements
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(...args)
|
||||
}
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
function dispatch (...args) {
|
||||
_dispatch.apply(null, args)
|
||||
}
|
||||
|
||||
5
renderer/lib/hx.js
Normal file
5
renderer/lib/hx.js
Normal file
@@ -0,0 +1,5 @@
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
module.exports = hx
|
||||
95
renderer/lib/migrations.js
Normal file
95
renderer/lib/migrations.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
module.exports = {
|
||||
run
|
||||
}
|
||||
|
||||
var semver = require('semver')
|
||||
var config = require('../../config')
|
||||
|
||||
// Change `state.saved` (which will be saved back to config.json on exit) as
|
||||
// needed, for example to deal with config.json format changes across versions
|
||||
function run (state) {
|
||||
// Replace "{ version: 1 }" with app version (semver)
|
||||
if (!semver.valid(state.saved.version)) {
|
||||
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
|
||||
}
|
||||
|
||||
var version = state.saved.version
|
||||
|
||||
if (semver.lt(version, '0.7.0')) {
|
||||
migrate_0_7_0(state.saved)
|
||||
}
|
||||
|
||||
if (semver.lt(version, '0.7.2')) {
|
||||
migrate_0_7_2(state.saved)
|
||||
}
|
||||
|
||||
// Config is now on the new version
|
||||
state.saved.version = config.APP_VERSION
|
||||
}
|
||||
|
||||
function migrate_0_7_0 (saved) {
|
||||
console.log('migrate to 0.7.0')
|
||||
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
|
||||
saved.torrents.forEach(function (ts) {
|
||||
var infoHash = ts.infoHash
|
||||
|
||||
// Replace torrentPath with torrentFileName
|
||||
// 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
|
||||
var src, dst
|
||||
if (ts.torrentPath) {
|
||||
console.log('replacing torrentPath %s', ts.torrentPath)
|
||||
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||
src = ts.torrentPath
|
||||
} else {
|
||||
src = path.join(config.STATIC_PATH, ts.torrentPath)
|
||||
}
|
||||
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.torrentPath
|
||||
ts.torrentFileName = infoHash + '.torrent'
|
||||
}
|
||||
|
||||
// Replace posterURL with posterFileName
|
||||
if (ts.posterURL) {
|
||||
console.log('replacing posterURL %s', ts.posterURL)
|
||||
var extension = path.extname(ts.posterURL)
|
||||
src = path.isAbsolute(ts.posterURL)
|
||||
? ts.posterURL
|
||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||
dst = path.join(config.POSTER_PATH, infoHash + extension)
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.posterURL
|
||||
ts.posterFileName = infoHash + extension
|
||||
}
|
||||
|
||||
// Fix exception caused by incorrect file ordering.
|
||||
// https://github.com/feross/webtorrent-desktop/pull/604#issuecomment-222805214
|
||||
delete ts.defaultPlayFileIndex
|
||||
delete ts.files
|
||||
delete ts.selections
|
||||
delete ts.fileModtimes
|
||||
})
|
||||
}
|
||||
|
||||
function migrate_0_7_2 (saved) {
|
||||
if (!saved.prefs) {
|
||||
saved.prefs = {
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||
}
|
||||
}
|
||||
}
|
||||
212
renderer/lib/state.js
Normal file
212
renderer/lib/state.js
Normal file
@@ -0,0 +1,212 @@
|
||||
module.exports = {
|
||||
getDefaultPlayState,
|
||||
load,
|
||||
save
|
||||
}
|
||||
|
||||
var appConfig = require('application-config')('WebTorrent')
|
||||
var path = require('path')
|
||||
|
||||
var config = require('../../config')
|
||||
var migrations = require('./migrations')
|
||||
|
||||
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||
|
||||
function getDefaultState () {
|
||||
var LocationHistory = require('./location-history')
|
||||
|
||||
return {
|
||||
/*
|
||||
* Temporary state disappears once the program exits.
|
||||
* It can contain complex objects like open connections, etc.
|
||||
*/
|
||||
client: null, /* the WebTorrent client */
|
||||
server: null, /* local WebTorrent-to-HTTP server */
|
||||
prev: {}, /* used for state diffing in updateElectron() */
|
||||
location: new LocationHistory(),
|
||||
window: {
|
||||
bounds: null, /* {x, y, width, height } */
|
||||
isFocused: true,
|
||||
isFullScreen: false,
|
||||
title: config.APP_WINDOW_TITLE
|
||||
},
|
||||
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
||||
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
|
||||
devices: {}, /* playback devices like Chromecast and AppleTV */
|
||||
dock: {
|
||||
badge: 0,
|
||||
progress: 0
|
||||
},
|
||||
modal: null, /* modal popover */
|
||||
errors: [], /* user-facing errors */
|
||||
nextTorrentKey: 1, /* identify torrents for IPC between the main and webtorrent windows */
|
||||
|
||||
/*
|
||||
* Saved state is read from and written to a file every time the app runs.
|
||||
* It should be simple and minimal and must be JSON.
|
||||
* It must never contain absolute paths since we have a portable app.
|
||||
*
|
||||
* Config path:
|
||||
*
|
||||
* OS X ~/Library/Application Support/WebTorrent/config.json
|
||||
* Linux (XDG) $XDG_CONFIG_HOME/WebTorrent/config.json
|
||||
* Linux (Legacy) ~/.config/WebTorrent/config.json
|
||||
* Windows (> Vista) %LOCALAPPDATA%/WebTorrent/config.json
|
||||
* Windows (XP, 2000) %USERPROFILE%/Local Settings/Application Data/WebTorrent/config.json
|
||||
*
|
||||
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
||||
*/
|
||||
saved: {},
|
||||
|
||||
/*
|
||||
* Getters, for convenience
|
||||
*/
|
||||
getPlayingTorrentSummary,
|
||||
getPlayingFileSummary
|
||||
}
|
||||
}
|
||||
|
||||
/* Whenever we stop playing video or audio, here's what we reset state.playing to */
|
||||
function getDefaultPlayState () {
|
||||
return {
|
||||
infoHash: null, /* the info hash of the torrent we're playing */
|
||||
fileIndex: null, /* the zero-based index within the torrent */
|
||||
location: 'local', /* 'local', 'chromecast', 'airplay' */
|
||||
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
|
||||
currentTime: 0, /* seconds */
|
||||
duration: 1, /* seconds */
|
||||
isPaused: true,
|
||||
isStalled: false,
|
||||
lastTimeUpdate: 0, /* Unix time in ms */
|
||||
mouseStationarySince: 0, /* Unix time in ms */
|
||||
playbackRate: 1,
|
||||
subtitles: {
|
||||
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
||||
selectedIndex: -1, /* current subtitle track */
|
||||
showMenu: false /* popover menu, above the video */
|
||||
},
|
||||
aspectRatio: 0 /* aspect ratio of the video */
|
||||
}
|
||||
}
|
||||
|
||||
/* If the saved state file doesn't exist yet, here's what we use instead */
|
||||
function setupSavedState (cb) {
|
||||
var fs = require('fs-extra')
|
||||
var parseTorrent = require('parse-torrent')
|
||||
var parallel = require('run-parallel')
|
||||
|
||||
var saved = {
|
||||
prefs: {
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||
},
|
||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||
}
|
||||
|
||||
var tasks = []
|
||||
|
||||
config.DEFAULT_TORRENTS.map(function (t, i) {
|
||||
var infoHash = saved.torrents[i].infoHash
|
||||
tasks.push(function (cb) {
|
||||
fs.copy(
|
||||
path.join(config.STATIC_PATH, t.posterFileName),
|
||||
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)),
|
||||
cb
|
||||
)
|
||||
})
|
||||
tasks.push(function (cb) {
|
||||
fs.copy(
|
||||
path.join(config.STATIC_PATH, t.torrentFileName),
|
||||
path.join(config.TORRENT_PATH, infoHash + '.torrent'),
|
||||
cb
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
parallel(tasks, function (err) {
|
||||
if (err) return cb(err)
|
||||
cb(null, saved)
|
||||
})
|
||||
|
||||
function createTorrentObject (t) {
|
||||
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
||||
var parsedTorrent = parseTorrent(torrent)
|
||||
|
||||
return {
|
||||
status: 'paused',
|
||||
infoHash: parsedTorrent.infoHash,
|
||||
name: t.name,
|
||||
displayName: t.name,
|
||||
posterFileName: parsedTorrent.infoHash + path.extname(t.posterFileName),
|
||||
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
||||
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
||||
files: parsedTorrent.files,
|
||||
selections: parsedTorrent.files.map((x) => true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPlayingTorrentSummary () {
|
||||
var infoHash = this.playing.infoHash
|
||||
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||
}
|
||||
|
||||
function getPlayingFileSummary () {
|
||||
var torrentSummary = this.getPlayingTorrentSummary()
|
||||
if (!torrentSummary) return null
|
||||
return torrentSummary.files[this.playing.fileIndex]
|
||||
}
|
||||
|
||||
function load (cb) {
|
||||
var state = getDefaultState()
|
||||
|
||||
appConfig.read(function (err, saved) {
|
||||
if (err || !saved.version) {
|
||||
console.log('Missing config file: Creating new one')
|
||||
setupSavedState(onSaved)
|
||||
} else {
|
||||
onSaved(null, saved)
|
||||
}
|
||||
})
|
||||
|
||||
function onSaved (err, saved) {
|
||||
if (err) return cb(err)
|
||||
state.saved = saved
|
||||
migrations.run(state)
|
||||
cb(null, state)
|
||||
}
|
||||
}
|
||||
|
||||
// Write state.saved to the JSON state file
|
||||
function save (state, cb) {
|
||||
console.log('Saving state to ' + appConfig.filePath)
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
// Clean up, so that we're not saving any pending state
|
||||
var copy = Object.assign({}, state.saved)
|
||||
// Remove torrents pending addition to the list, where we haven't finished
|
||||
// reading the torrent file or file(s) to seed & don't have an infohash
|
||||
copy.torrents = copy.torrents
|
||||
.filter((x) => x.infoHash)
|
||||
.map(function (x) {
|
||||
var torrent = {}
|
||||
for (var key in x) {
|
||||
if (key === 'progress' || key === 'torrentKey') {
|
||||
continue // Don't save progress info or key for the webtorrent process
|
||||
}
|
||||
if (key === 'playStatus') {
|
||||
continue // Don't save whether a torrent is playing / pending
|
||||
}
|
||||
torrent[key] = x[key]
|
||||
}
|
||||
return torrent
|
||||
})
|
||||
|
||||
appConfig.write(copy, function (err) {
|
||||
if (err) console.error(err)
|
||||
|
||||
// TODO: this doesn't belong here
|
||||
electron.ipcRenderer.send('savedState')
|
||||
})
|
||||
}
|
||||
140
renderer/lib/telemetry.js
Normal file
140
renderer/lib/telemetry.js
Normal file
@@ -0,0 +1,140 @@
|
||||
// Collects anonymous usage stats and uncaught errors
|
||||
// Reports back so that we can improve WebTorrent Desktop
|
||||
module.exports = {
|
||||
init,
|
||||
logUncaughtError,
|
||||
logPlayAttempt
|
||||
}
|
||||
|
||||
const crypto = require('crypto')
|
||||
const electron = require('electron')
|
||||
const https = require('https')
|
||||
const os = require('os')
|
||||
const url = require('url')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
var telemetry
|
||||
|
||||
function init (state) {
|
||||
telemetry = state.saved.telemetry
|
||||
if (!telemetry) {
|
||||
telemetry = state.saved.telemetry = createSummary()
|
||||
reset()
|
||||
}
|
||||
|
||||
var now = new Date()
|
||||
telemetry.timestamp = now.toISOString()
|
||||
telemetry.localTime = now.toTimeString()
|
||||
telemetry.screens = getScreenInfo()
|
||||
telemetry.system = getSystemInfo()
|
||||
telemetry.approxNumTorrents = getApproxNumTorrents(state)
|
||||
|
||||
postToServer(telemetry)
|
||||
}
|
||||
|
||||
function reset () {
|
||||
telemetry.uncaughtErrors = []
|
||||
telemetry.playAttempts = {
|
||||
total: 0,
|
||||
success: 0,
|
||||
timeout: 0,
|
||||
error: 0,
|
||||
abandoned: 0
|
||||
}
|
||||
}
|
||||
|
||||
function postToServer () {
|
||||
// Serialize the telemetry summary
|
||||
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
|
||||
|
||||
// POST to our server
|
||||
var options = url.parse(config.TELEMETRY_URL)
|
||||
options.method = 'POST'
|
||||
options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': payload.length
|
||||
}
|
||||
|
||||
var req = https.request(options, function (res) {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('Successfully posted telemetry summary')
|
||||
reset()
|
||||
} else {
|
||||
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
|
||||
}
|
||||
})
|
||||
req.on('error', function (e) {
|
||||
console.error('Couldn\'t post telemetry summary', e)
|
||||
})
|
||||
req.write(payload)
|
||||
req.end()
|
||||
}
|
||||
|
||||
// Creates a new telemetry summary. Gives the user a unique ID,
|
||||
// collects screen resolution, etc
|
||||
function createSummary () {
|
||||
// Make a 256-bit random unique ID
|
||||
var userID = crypto.randomBytes(32).toString('hex')
|
||||
return { userID }
|
||||
}
|
||||
|
||||
// Track screen resolution
|
||||
function getScreenInfo () {
|
||||
return electron.screen.getAllDisplays().map((screen) => ({
|
||||
width: screen.size.width,
|
||||
height: screen.size.height,
|
||||
scaleFactor: screen.scaleFactor
|
||||
}))
|
||||
}
|
||||
|
||||
// Track basic system info like OS version and amount of RAM
|
||||
function getSystemInfo () {
|
||||
return {
|
||||
osPlatform: process.platform,
|
||||
osRelease: os.type() + ' ' + os.release(),
|
||||
architecture: os.arch(),
|
||||
totalMemoryMB: os.totalmem() / (1 << 20),
|
||||
numCores: os.cpus().length
|
||||
}
|
||||
}
|
||||
|
||||
// Get the number of torrents, rounded to the nearest power of two
|
||||
function getApproxNumTorrents (state) {
|
||||
var exactNum = state.saved.torrents.length
|
||||
if (exactNum === 0) return 0
|
||||
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||
var log2 = Math.log(exactNum) / Math.log(2)
|
||||
return 1 << Math.round(log2)
|
||||
}
|
||||
|
||||
// An uncaught error happened in the main process or in one of the windows
|
||||
function logUncaughtError (procName, err) {
|
||||
var message, stack
|
||||
if (typeof err === 'string') {
|
||||
message = err
|
||||
stack = ''
|
||||
} else {
|
||||
message = err.message
|
||||
stack = err.stack
|
||||
}
|
||||
|
||||
// We need to POST the telemetry object, make sure it stays < 100kb
|
||||
if (telemetry.uncaughtErrors.length > 20) return
|
||||
if (message.length > 1000) message = message.substring(0, 1000)
|
||||
if (stack.length > 1000) stack = stack.substring(0, 1000)
|
||||
|
||||
telemetry.uncaughtErrors.push({process: procName, message, stack})
|
||||
}
|
||||
|
||||
// The user pressed play. It either worked, timed out, or showed the
|
||||
// "Play in VLC" codec error
|
||||
function logPlayAttempt (result) {
|
||||
if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) {
|
||||
return console.error('Unknown play attempt result', result)
|
||||
}
|
||||
|
||||
var attempts = telemetry.playAttempts
|
||||
attempts.total = (attempts.total || 0) + 1
|
||||
attempts[result] = (attempts[result] || 0) + 1
|
||||
}
|
||||
@@ -24,7 +24,8 @@ function isVideo (file) {
|
||||
'.mp4',
|
||||
'.mpg',
|
||||
'.ogv',
|
||||
'.webm'
|
||||
'.webm',
|
||||
'.wmv'
|
||||
].includes(ext)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,19 @@ var captureVideoFrame = require('./capture-video-frame')
|
||||
var path = require('path')
|
||||
|
||||
function torrentPoster (torrent, cb) {
|
||||
// First, try to use the largest video file
|
||||
// First, try to use a poster image if available
|
||||
var posterFile = torrent.files.filter(function (file) {
|
||||
return /^poster\.(jpg|png|gif)$/.test(file.name)
|
||||
})[0]
|
||||
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
|
||||
|
||||
// Second, try to use the largest video file
|
||||
// Filter out file formats that the <video> tag definitely can't play
|
||||
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||
|
||||
// Second, try to use the largest image file
|
||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
|
||||
// Third, try to use the largest image file
|
||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
|
||||
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
||||
|
||||
// TODO: generate a waveform from the largest sound file
|
||||
|
||||
@@ -10,14 +10,14 @@ var config = require('../../config')
|
||||
// Returns an absolute path to the torrent file, or null if unavailable
|
||||
function getTorrentPath (torrentSummary) {
|
||||
if (!torrentSummary || !torrentSummary.torrentFileName) return null
|
||||
return path.join(config.CONFIG_TORRENT_PATH, torrentSummary.torrentFileName)
|
||||
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
|
||||
}
|
||||
|
||||
// Expects a torrentSummary
|
||||
// Returns an absolute path to the poster image, or null if unavailable
|
||||
function getPosterPath (torrentSummary) {
|
||||
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
||||
var posterPath = path.join(config.CONFIG_POSTER_PATH, torrentSummary.posterFileName)
|
||||
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
|
||||
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||
return posterPath.replace(/\\/g, '/')
|
||||
|
||||
@@ -50,19 +50,22 @@ table {
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.app {
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
animation: fadein 0.3s;
|
||||
background: rgb(40, 40, 40);
|
||||
animation: fadein 0.5s;
|
||||
}
|
||||
|
||||
.app:not(.is-focused) {
|
||||
@@ -94,11 +97,20 @@ table {
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
opacity: 0.85;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.icon.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.icon:not(.disabled):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* UTILITY CLASSES
|
||||
*/
|
||||
@@ -109,8 +121,8 @@ table {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.3;
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
@@ -144,8 +156,8 @@ table {
|
||||
.header {
|
||||
background: rgb(40, 40, 40);
|
||||
border-bottom: 1px solid rgb(20, 20, 20);
|
||||
height: 37px; /* vertically center OS menu buttons (OS X) */
|
||||
padding-top: 6px;
|
||||
height: 38px; /* vertically center OS menu buttons (OS X) */
|
||||
padding-top: 7px;
|
||||
overflow: hidden;
|
||||
flex: 0 1 auto;
|
||||
opacity: 1;
|
||||
@@ -164,7 +176,13 @@ table {
|
||||
}
|
||||
|
||||
.app.view-player .header {
|
||||
opacity: 0.8;
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.app.view-player.is-win32 .header,
|
||||
.app.view-player.is-linux .header {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.app.hide-video-controls.view-player .header {
|
||||
@@ -172,12 +190,8 @@ table {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.app.hide-header .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
opacity: 0.6;
|
||||
opacity: 0.7;
|
||||
position: absolute;
|
||||
margin-top: 1px;
|
||||
padding: 0 150px 0 150px;
|
||||
@@ -188,35 +202,22 @@ table {
|
||||
|
||||
.header .nav {
|
||||
font-weight: bold;
|
||||
margin-right: 9px;
|
||||
}
|
||||
|
||||
.header .nav.left {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.header .nav.right {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.app.is-darwin:not(.is-fullscreen) .header .nav.left {
|
||||
margin-left: 78px;
|
||||
}
|
||||
|
||||
.header .nav.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.header .nav * {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.header .nav .disabled {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.header .nav *:not(.disabled):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.header .nav .back,
|
||||
.header .nav .forward {
|
||||
.header .back,
|
||||
.header .forward {
|
||||
font-size: 30px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
@@ -259,7 +260,7 @@ table {
|
||||
|
||||
.modal .modal-content {
|
||||
position: fixed;
|
||||
top: 45px;
|
||||
top: 38px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
@@ -279,36 +280,36 @@ table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-torrent-page {
|
||||
.create-torrent {
|
||||
padding: 10px 25px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute {
|
||||
.create-torrent .torrent-attribute {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute>* {
|
||||
.create-torrent .torrent-attribute>* {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute label {
|
||||
.create-torrent .torrent-attribute label {
|
||||
width: 60px;
|
||||
margin-right: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute>div {
|
||||
.create-torrent .torrent-attribute>div {
|
||||
width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute div {
|
||||
.create-torrent .torrent-attribute div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute textarea {
|
||||
.create-torrent .torrent-attribute textarea {
|
||||
width: calc(100% - 80px);
|
||||
height: 80px;
|
||||
color: #eee;
|
||||
@@ -320,11 +321,11 @@ table {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.create-torrent-page textarea.torrent-trackers {
|
||||
.create-torrent textarea.torrent-trackers {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.create-torrent-page input.torrent-is-private {
|
||||
.create-torrent input.torrent-is-private {
|
||||
width: initial;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -535,6 +536,11 @@ input {
|
||||
}
|
||||
}
|
||||
|
||||
.torrent .buttons .play.resume-position {
|
||||
position: relative;
|
||||
-webkit-clip-path: circle(18px at center);
|
||||
}
|
||||
|
||||
.torrent .buttons .delete {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -543,6 +549,10 @@ input {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.torrent .buttons .radial-progress {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.torrent .name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
@@ -592,7 +602,11 @@ body.drag .app::after {
|
||||
}
|
||||
|
||||
.torrent-details {
|
||||
padding: 8em 12px 20px 20px;
|
||||
padding: 8em 0 20px 0;
|
||||
}
|
||||
|
||||
.torrent-details .warning {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.torrent-details table {
|
||||
@@ -617,7 +631,7 @@ body.drag .app::after {
|
||||
.torrent-details td {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
vertical-align: bottom;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.torrent-details td .icon {
|
||||
@@ -627,7 +641,14 @@ body.drag .app::after {
|
||||
}
|
||||
|
||||
.torrent-details td.col-icon {
|
||||
width: 2em;
|
||||
width: 3em;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.torrent-details td.col-icon .radial-progress {
|
||||
position: absolute;
|
||||
margin-top: 4px;
|
||||
margin-left: 0.5px;
|
||||
}
|
||||
|
||||
.torrent-details td.col-name {
|
||||
@@ -646,7 +667,8 @@ body.drag .app::after {
|
||||
}
|
||||
|
||||
.torrent-details td.col-select {
|
||||
width: 2em;
|
||||
width: 3em;
|
||||
padding-right: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -678,7 +700,7 @@ body.drag .app::after {
|
||||
* PLAYER CONTROLS
|
||||
*/
|
||||
|
||||
.player-controls {
|
||||
.player .controls {
|
||||
position: fixed;
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
width: 100%;
|
||||
@@ -687,7 +709,60 @@ body.drag .app::after {
|
||||
transition: opacity 0.15s ease-out;
|
||||
}
|
||||
|
||||
.app.hide-video-controls .player-controls {
|
||||
.player .controls .icon {
|
||||
display: block;
|
||||
margin: 8px;
|
||||
font-size: 22px;
|
||||
opacity: 0.85;
|
||||
|
||||
/* Make all icons have uniform spacing */
|
||||
max-width: 23px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.player .controls .icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.player .controls .play-pause {
|
||||
font-size: 28px;
|
||||
margin-top: 5px;
|
||||
margin-right: 10px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.player .controls .volume-slider {
|
||||
-webkit-appearance: none;
|
||||
-webkit-app-region: no-drag;
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
margin: 18px 8px 8px 0;
|
||||
border: none;
|
||||
padding: 0;
|
||||
opacity: 0.85;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.player .controls .time,
|
||||
.player .controls .rate {
|
||||
font-weight: 100;
|
||||
font-size: 13px;
|
||||
margin: 9px 8px 8px 8px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.player .controls .icon.closed-caption {
|
||||
font-size: 26px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.player .controls .icon.fullscreen {
|
||||
font-size: 26px;
|
||||
margin-right: 15px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.app.hide-video-controls .player .controls {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -695,13 +770,16 @@ body.drag .app::after {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.app.hide-video-controls .player .player-controls:hover {
|
||||
/* TODO: find better way to handle this (that also
|
||||
* keeps the header visible too).
|
||||
*/
|
||||
.app.hide-video-controls .player .controls:hover {
|
||||
opacity: 1;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* invisible click target for scrubbing */
|
||||
.player-controls .scrub-bar {
|
||||
.player .controls .scrub-bar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 23px; /* 3px .loading-bar plus 10px above and below */
|
||||
@@ -710,7 +788,7 @@ body.drag .app::after {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.player-controls .loading-bar {
|
||||
.player .controls .loading-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
top: -3px;
|
||||
@@ -720,14 +798,14 @@ body.drag .app::after {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.player-controls .loading-bar-part {
|
||||
.player .controls .loading-bar-part {
|
||||
position: absolute;
|
||||
background-color: #dd0000;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.player-controls .playback-cursor {
|
||||
.player .controls .playback-cursor {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
background-color: #FFF;
|
||||
@@ -736,94 +814,26 @@ body.drag .app::after {
|
||||
border-radius: 50%;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
transition-property: width, height, border-radius, margin-top, margin-left;
|
||||
transition-property: width, height, top, margin-left;
|
||||
transition-duration: 0.1s;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.player-controls .play-pause {
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 5px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.player-controls .device,
|
||||
.player-controls .fullscreen,
|
||||
.player-controls .closed-captions,
|
||||
.player-controls .volume-icon,
|
||||
.player-controls .back {
|
||||
display: block;
|
||||
height: 20px;
|
||||
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 .back {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.player-controls .device,
|
||||
.player-controls .closed-captions,
|
||||
.player-controls .fullscreen {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.player-controls .fullscreen {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.player-controls .volume-icon,
|
||||
.player-controls .device {
|
||||
font-size: 18px; /* make the cast icons less huge */
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
.player-controls .closed-captions.active,
|
||||
.player-controls .device.active {
|
||||
.player .controls .closed-caption.active,
|
||||
.player .controls .device.active {
|
||||
color: #9af;
|
||||
}
|
||||
|
||||
.player-controls .volume {
|
||||
display: block;
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.player-controls .volume-icon {
|
||||
float: left;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.player-controls .volume-slider {
|
||||
.player .controls .volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 50px;
|
||||
height: 3px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
vertical-align: sub;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.player-controls .volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
background-color: #fff;
|
||||
opacity: 1.0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 1px solid #303233;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.player-controls .volume-slider:focus {
|
||||
.player .controls .volume-slider:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -833,19 +843,49 @@ body.drag .app::after {
|
||||
|
||||
.player .playback-bar:hover .playback-cursor {
|
||||
top: -8px;
|
||||
margin-left: -5px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.player .controls .options-list {
|
||||
position: fixed;
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
min-width: 100px;
|
||||
bottom: 45px;
|
||||
right: 3px;
|
||||
transition: opacity 0.15s ease-out;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.player .controls .options-list .icon {
|
||||
display: inline;
|
||||
font-size: 17px;
|
||||
vertical-align: bottom;
|
||||
line-height: 21px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cue text position so it appears above the player controls.
|
||||
*/
|
||||
video::-webkit-media-text-track-container {
|
||||
bottom: 60px;
|
||||
transition: bottom 0.1s ease-out;
|
||||
}
|
||||
.app.hide-video-controls video::-webkit-media-text-track-container {
|
||||
bottom: 30px;
|
||||
}
|
||||
::cue {
|
||||
background: none;
|
||||
color: #FFF;
|
||||
font: 24px;
|
||||
line-height: 1.3em;
|
||||
text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* CHROMECAST / AIRPLAY CONTROLS
|
||||
*/
|
||||
@@ -873,28 +913,172 @@ body.drag .app::after {
|
||||
}
|
||||
|
||||
/*
|
||||
* Subtitles list
|
||||
* Preferences page, based on Atom settings style
|
||||
*/
|
||||
|
||||
.subtitles-list {
|
||||
position: fixed;
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
min-width: 100px;
|
||||
bottom: 45px;
|
||||
right: 3px;
|
||||
transition: opacity 0.15s ease-out;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
.preferences {
|
||||
font-size: 12px;
|
||||
line-height: calc(10/7);
|
||||
}
|
||||
|
||||
.subtitles-list i {
|
||||
font-size: 11px; /* make the cast icons less huge */
|
||||
margin-right: 4px !important;
|
||||
.preferences .text {
|
||||
color: #a8a8a8;
|
||||
}
|
||||
|
||||
.preferences .icon {
|
||||
color: rgba(170, 170, 170, 0.6);
|
||||
font-size: 16px;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
.preferences .btn {
|
||||
display: inline-block;
|
||||
-webkit-appearance: button;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border-color: #cccccc;
|
||||
border-radius: 3px;
|
||||
color: #9da5b4;
|
||||
text-shadow: none;
|
||||
border: 1px solid #181a1f;
|
||||
background-color: #3d3d3d;
|
||||
white-space: initial;
|
||||
font-size: 0.889em;
|
||||
line-height: 1;
|
||||
padding: 0.5em 0.75em;
|
||||
}
|
||||
|
||||
.preferences .btn .icon {
|
||||
margin: 0;
|
||||
color: #a8a8a8;
|
||||
}
|
||||
|
||||
.preferences .help .icon {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
|
||||
.preferences .preferences-panel .control-group + .control-group {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.preferences .section {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #181a1f;
|
||||
}
|
||||
|
||||
.preferences .section:first {
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.preferences .section:first-child,
|
||||
.preferences .section:last-child {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preferences .section.section:empty {
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.preferences .section-container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.preferences section .section-heading,
|
||||
.preferences .section .section-heading {
|
||||
margin-bottom: 10px;
|
||||
color: #dcdcdc;
|
||||
font-size: 1.75em;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.preferences .sub-section-heading.icon:before,
|
||||
.preferences .section-heading.icon:before {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.preferences .section-heading-count {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.preferences .section-body {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.preferences .sub-section {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.preferences .sub-section .sub-section-heading {
|
||||
color: #dcdcdc;
|
||||
font-size: 1.4em;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.preferences .preferences-panel label {
|
||||
color: #a8a8a8;
|
||||
}
|
||||
|
||||
.preferences .preferences-panel .control-group + .control-group {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.preferences .preferences-panel .control-group .editor-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preferences .preference-title {
|
||||
font-size: 1.2em;
|
||||
-webkit-user-select: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.preferences .preference-description {
|
||||
color: rgba(170, 170, 170, 0.6);
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.preferences input {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.15em;
|
||||
max-height: none;
|
||||
width: 100%;
|
||||
padding-left: 0.5em;
|
||||
border-radius: 3px;
|
||||
color: #a8a8a8;
|
||||
border: 1px solid #181a1f;
|
||||
background-color: #1b1d23;
|
||||
}
|
||||
|
||||
.preferences input::-webkit-input-placeholder {
|
||||
color: rgba(170, 170, 170, 0.6);
|
||||
}
|
||||
|
||||
.preferences .control-group input {
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
.preferences .control-group input.file-picker-text {
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.preferences .control-group .checkbox .icon {
|
||||
font-size: 1.5em;
|
||||
margin: 0;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* MEDIA OVERLAY / AUDIO DETAILS
|
||||
*/
|
||||
@@ -963,10 +1147,6 @@ body.drag .app::after {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.app.hide-header .error-popover {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.error-popover.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -994,3 +1174,66 @@ body.drag .app::after {
|
||||
.error-text {
|
||||
color: #c44;
|
||||
}
|
||||
|
||||
/*
|
||||
* RADIAL PROGRESS BAR
|
||||
*/
|
||||
|
||||
.radial-progress {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #888;
|
||||
}
|
||||
|
||||
.radial-progress .circle .mask,
|
||||
.radial-progress .circle .fill {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.radial-progress .circle .mask {
|
||||
clip: rect(0px, 16px, 16px, 8px);
|
||||
}
|
||||
|
||||
.radial-progress .circle .fill {
|
||||
clip: rect(0px, 8px, 16px, 0px);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.radial-progress .inset {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 2px 0 0 2px;
|
||||
border-radius: 50%;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.radial-progress-large {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.radial-progress-large .circle .mask,
|
||||
.radial-progress-large .circle .fill {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.radial-progress-large .circle .mask {
|
||||
clip: rect(0px, 40px, 40px, 20px);
|
||||
}
|
||||
|
||||
.radial-progress-large .circle .fill {
|
||||
clip: rect(0px, 20px, 40px, 0px);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.radial-progress-large .inset {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 4px 0 0 4px;
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="index.css" charset="utf-8">
|
||||
<link rel="stylesheet" href="main.css" charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<script async src="index.js"></script>
|
||||
<script async src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,21 +3,9 @@ console.time('init')
|
||||
var crashReporter = require('../crash-reporter')
|
||||
crashReporter.init()
|
||||
|
||||
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 electron = require('electron')
|
||||
var fs = require('fs-extra')
|
||||
var iso639 = require('iso-639-1')
|
||||
var mainLoop = require('main-loop')
|
||||
var parallel = require('run-parallel')
|
||||
var path = require('path')
|
||||
@@ -26,42 +14,47 @@ var createElement = require('virtual-dom/create-element')
|
||||
var diff = require('virtual-dom/diff')
|
||||
var patch = require('virtual-dom/patch')
|
||||
|
||||
var App = require('./views/app')
|
||||
var config = require('../config')
|
||||
var App = require('./views/app')
|
||||
var telemetry = require('./lib/telemetry')
|
||||
var errors = require('./lib/errors')
|
||||
var sound = require('./lib/sound')
|
||||
var State = require('./state')
|
||||
var State = require('./lib/state')
|
||||
var TorrentPlayer = require('./lib/torrent-player')
|
||||
var TorrentSummary = require('./lib/torrent-summary')
|
||||
|
||||
var {setDispatch} = require('./lib/dispatcher')
|
||||
setDispatch(dispatch)
|
||||
|
||||
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 state, vdomLoop
|
||||
|
||||
// This dependency is the slowest-loading, so we lazy load it
|
||||
var Cast = null
|
||||
|
||||
// For easy debugging in Developer Tools
|
||||
var state = global.state = State.getInitialState()
|
||||
init()
|
||||
|
||||
// Push the first page into the location history
|
||||
state.location.go({ url: 'home' })
|
||||
function init () {
|
||||
// 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.
|
||||
State.load(onState)
|
||||
|
||||
var vdomLoop
|
||||
|
||||
// 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.
|
||||
loadState(init)
|
||||
setDispatch(dispatch)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once when the application loads. (Not once per window.)
|
||||
* Connects to the torrent networks, sets up the UI and OS integrations like
|
||||
* the dock icon and drag+drop.
|
||||
*/
|
||||
function init () {
|
||||
// Clean up the freshly-loaded config file, which may be from an older version
|
||||
cleanUpConfig()
|
||||
function onState (err, _state) {
|
||||
if (err) return onError(err)
|
||||
state = _state
|
||||
|
||||
// Add first page to location history
|
||||
state.location.go({ url: 'home' })
|
||||
|
||||
// Restart everything we were torrenting last time the app ran
|
||||
resumeTorrents()
|
||||
@@ -80,6 +73,9 @@ function init () {
|
||||
})
|
||||
document.body.appendChild(vdomLoop.target)
|
||||
|
||||
// Listen for messages from the main process
|
||||
setupIpc()
|
||||
|
||||
// Calling update() updates the UI given the current state
|
||||
// Do this at least once a second to give every file in every torrentSummary
|
||||
// a progress bar and to keep the cursor in sync when playing a video
|
||||
@@ -96,69 +92,22 @@ function init () {
|
||||
window.addEventListener('focus', onFocus)
|
||||
window.addEventListener('blur', onBlur)
|
||||
|
||||
// Done! Ideally we want to get here <100ms after the user clicks the app
|
||||
sound.play('STARTUP')
|
||||
// ...window visibility state.
|
||||
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
||||
|
||||
// Log uncaught JS errors
|
||||
window.addEventListener('error',
|
||||
(e) => telemetry.logUncaughtError('window', e.error), true)
|
||||
|
||||
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||
sound.play('STARTUP')
|
||||
console.timeEnd('init')
|
||||
}
|
||||
|
||||
function delayedInit () {
|
||||
lazyLoadCast()
|
||||
sound.preload()
|
||||
}
|
||||
|
||||
// Change `state.saved` (which will be saved back to config.json on exit) as
|
||||
// needed, for example to deal with config.json format changes across versions
|
||||
function cleanUpConfig () {
|
||||
state.saved.torrents.forEach(function (ts) {
|
||||
var infoHash = ts.infoHash
|
||||
|
||||
// Migration: replace torrentPath with torrentFileName
|
||||
var src, dst
|
||||
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)
|
||||
if (path.isAbsolute(ts.torrentPath)) {
|
||||
src = 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')
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.torrentPath
|
||||
ts.torrentFileName = infoHash + '.torrent'
|
||||
}
|
||||
|
||||
// Migration: replace posterURL with posterFileName
|
||||
if (ts.posterURL) {
|
||||
console.log('migration: replacing posterURL %s', ts.posterURL)
|
||||
var extension = path.extname(ts.posterURL)
|
||||
src = path.isAbsolute(ts.posterURL)
|
||||
? ts.posterURL
|
||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||
dst = path.join(config.CONFIG_POSTER_PATH, infoHash + extension)
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.posterURL
|
||||
ts.posterFileName = infoHash + extension
|
||||
}
|
||||
|
||||
// Migration: add per-file selections
|
||||
if (!ts.selections) {
|
||||
ts.selections = ts.files.map((x) => true)
|
||||
}
|
||||
})
|
||||
telemetry.init(state)
|
||||
}
|
||||
|
||||
// Lazily loads Chromecast and Airplay support
|
||||
@@ -215,17 +164,23 @@ function dispatch (action, ...args) {
|
||||
if (action === 'addTorrent') {
|
||||
addTorrent(args[0] /* torrent */)
|
||||
}
|
||||
if (action === 'showOpenTorrentFile') {
|
||||
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
|
||||
if (action === 'openTorrentFile') {
|
||||
ipcRenderer.send('openTorrentFile') /* open torrent file */
|
||||
}
|
||||
if (action === 'openFiles') {
|
||||
ipcRenderer.send('openFiles') /* add files with dialog */
|
||||
}
|
||||
if (action === 'showCreateTorrent') {
|
||||
showCreateTorrent(args[0] /* fileOrFolder */)
|
||||
showCreateTorrent(args[0] /* paths */)
|
||||
}
|
||||
if (action === 'openTorrentAddress') {
|
||||
state.modal = { id: 'open-torrent-address-modal' }
|
||||
}
|
||||
if (action === 'createTorrent') {
|
||||
createTorrent(args[0] /* options */)
|
||||
}
|
||||
if (action === 'openFile') {
|
||||
openFile(args[0] /* infoHash */, args[1] /* index */)
|
||||
if (action === 'openItem') {
|
||||
openItem(args[0] /* infoHash */, args[1] /* index */)
|
||||
}
|
||||
if (action === 'toggleTorrent') {
|
||||
toggleTorrent(args[0] /* infoHash */)
|
||||
@@ -242,11 +197,14 @@ function dispatch (action, ...args) {
|
||||
if (action === 'openTorrentContextMenu') {
|
||||
openTorrentContextMenu(args[0] /* infoHash */)
|
||||
}
|
||||
if (action === 'openDevice') {
|
||||
lazyLoadCast().open(args[0] /* deviceType */)
|
||||
if (action === 'toggleCastMenu') {
|
||||
lazyLoadCast().toggleMenu(args[0] /* deviceType */)
|
||||
}
|
||||
if (action === 'closeDevice') {
|
||||
lazyLoadCast().close()
|
||||
if (action === 'selectCastDevice') {
|
||||
lazyLoadCast().selectDevice(args[0] /* index */)
|
||||
}
|
||||
if (action === 'stopCasting') {
|
||||
lazyLoadCast().stop()
|
||||
}
|
||||
if (action === 'setDimensions') {
|
||||
setDimensions(args[0] /* dimensions */)
|
||||
@@ -273,18 +231,17 @@ function dispatch (action, ...args) {
|
||||
playPause()
|
||||
}
|
||||
if (action === 'play') {
|
||||
state.location.go({
|
||||
url: 'player',
|
||||
onbeforeload: function (cb) {
|
||||
play()
|
||||
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
|
||||
},
|
||||
onbeforeunload: closePlayer
|
||||
})
|
||||
playFile(args[0] /* infoHash */, args[1] /* index */)
|
||||
}
|
||||
if (action === 'playbackJump') {
|
||||
jumpToTime(args[0] /* seconds */)
|
||||
}
|
||||
if (action === 'skip') {
|
||||
jumpToTime(state.playing.currentTime + (args[0] /* direction */ * 10))
|
||||
}
|
||||
if (action === 'changePlaybackRate') {
|
||||
changePlaybackRate(args[0] /* direction */)
|
||||
}
|
||||
if (action === 'changeVolume') {
|
||||
changeVolume(args[0] /* increase */)
|
||||
}
|
||||
@@ -305,6 +262,7 @@ function dispatch (action, ...args) {
|
||||
}
|
||||
if (action === 'mediaError') {
|
||||
if (state.location.url() === 'player') {
|
||||
state.playing.result = 'error'
|
||||
state.playing.location = 'error'
|
||||
ipcRenderer.send('checkForVLC')
|
||||
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
||||
@@ -316,6 +274,9 @@ function dispatch (action, ...args) {
|
||||
})
|
||||
}
|
||||
}
|
||||
if (action === 'mediaSuccess') {
|
||||
state.playing.result = 'success'
|
||||
}
|
||||
if (action === 'mediaTimeUpdate') {
|
||||
state.playing.lastTimeUpdate = new Date().getTime()
|
||||
state.playing.isStalled = false
|
||||
@@ -338,6 +299,26 @@ function dispatch (action, ...args) {
|
||||
if (action === 'exitModal') {
|
||||
state.modal = null
|
||||
}
|
||||
if (action === 'preferences') {
|
||||
state.location.go({
|
||||
url: 'preferences',
|
||||
onbeforeload: function (cb) {
|
||||
// initialize preferences
|
||||
state.window.title = 'Preferences'
|
||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||
cb()
|
||||
},
|
||||
onbeforeunload: function (cb) {
|
||||
// save state after preferences
|
||||
savePreferences()
|
||||
state.window.title = config.APP_WINDOW_TITLE
|
||||
cb()
|
||||
}
|
||||
})
|
||||
}
|
||||
if (action === 'updatePreferences') {
|
||||
updatePreferences(args[0], args[1] /* property, value */)
|
||||
}
|
||||
if (action === 'updateAvailable') {
|
||||
updateAvailable(args[0] /* version */)
|
||||
}
|
||||
@@ -347,7 +328,13 @@ function dispatch (action, ...args) {
|
||||
saveStateThrottled()
|
||||
}
|
||||
if (action === 'saveState') {
|
||||
saveState()
|
||||
State.save(state)
|
||||
}
|
||||
if (action === 'setTitle') {
|
||||
state.window.title = args[0] /* title */
|
||||
}
|
||||
if (action === 'uncaughtError') {
|
||||
telemetry.logUncaughtError(args[0] /* process */, args[1] /* error */)
|
||||
}
|
||||
|
||||
// Update the virtual-dom, unless it's just a mouse move event
|
||||
@@ -385,11 +372,18 @@ function pause () {
|
||||
|
||||
function playPause () {
|
||||
if (state.location.url() !== 'player') return
|
||||
|
||||
if (state.playing.isPaused) {
|
||||
play()
|
||||
} else {
|
||||
pause()
|
||||
}
|
||||
|
||||
// force rerendering if window is hidden,
|
||||
// in order to bypass `raf` and play/pause media immediately
|
||||
if (!state.window.isVisible) render(state)
|
||||
|
||||
ipcRenderer.send('updateThumbnailBar', state.playing.isPaused)
|
||||
}
|
||||
|
||||
function jumpToTime (time) {
|
||||
@@ -399,7 +393,26 @@ function jumpToTime (time) {
|
||||
state.playing.jumpToTime = time
|
||||
}
|
||||
}
|
||||
|
||||
function changePlaybackRate (direction) {
|
||||
var rate = state.playing.playbackRate
|
||||
if (direction > 0 && rate >= 0.25 && rate < 2) {
|
||||
rate += 0.25
|
||||
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
||||
rate -= 0.25
|
||||
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
|
||||
rate = -1
|
||||
} else if (direction > 0 && rate === -1) {
|
||||
rate = 0.25
|
||||
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
|
||||
rate *= 2
|
||||
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
|
||||
rate /= 2
|
||||
}
|
||||
state.playing.playbackRate = rate
|
||||
if (lazyLoadCast().isCasting() && !Cast.setRate(rate)) {
|
||||
state.playing.playbackRate = 1
|
||||
}
|
||||
}
|
||||
function changeVolume (delta) {
|
||||
// change volume with delta value
|
||||
setVolume(state.playing.volume + delta)
|
||||
@@ -454,18 +467,11 @@ function isCasting () {
|
||||
}
|
||||
|
||||
function setupIpc () {
|
||||
ipcRenderer.send('ipcReady')
|
||||
|
||||
ipcRenderer.on('log', (e, ...args) => console.log(...args))
|
||||
ipcRenderer.on('error', (e, ...args) => console.error(...args))
|
||||
|
||||
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
|
||||
|
||||
ipcRenderer.on('showOpenTorrentAddress', function (e) {
|
||||
state.modal = { id: 'open-torrent-address-modal' }
|
||||
update()
|
||||
})
|
||||
|
||||
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
|
||||
state.window.isFullScreen = isFullScreen
|
||||
if (!isFullScreen) {
|
||||
@@ -487,29 +493,39 @@ function setupIpc () {
|
||||
ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args))
|
||||
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
|
||||
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
||||
}
|
||||
|
||||
// Load state.saved from the JSON state file
|
||||
function loadState (cb) {
|
||||
appConfig.read(function (err, data) {
|
||||
if (err) console.error(err)
|
||||
console.log('loaded state from ' + appConfig.filePath)
|
||||
ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err))
|
||||
|
||||
// populate defaults if they're not there
|
||||
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
|
||||
state.saved.torrents.forEach(function (torrentSummary) {
|
||||
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
|
||||
})
|
||||
|
||||
if (cb) cb()
|
||||
})
|
||||
ipcRenderer.send('ipcReady')
|
||||
}
|
||||
|
||||
// Starts all torrents that aren't paused on program startup
|
||||
function resumeTorrents () {
|
||||
state.saved.torrents
|
||||
.filter((x) => x.status !== 'paused')
|
||||
.forEach((x) => startTorrentingSummary(x))
|
||||
.filter((torrentSummary) => torrentSummary.status !== 'paused')
|
||||
.forEach((torrentSummary) => startTorrentingSummary(torrentSummary))
|
||||
}
|
||||
|
||||
// Updates a single property in the UNSAVED prefs
|
||||
// For example: updatePreferences("foo.bar", "baz")
|
||||
// Call savePreferences to save to config.json
|
||||
function updatePreferences (property, value) {
|
||||
var path = property.split('.')
|
||||
var key = state.unsaved.prefs
|
||||
for (var i = 0; i < path.length - 1; i++) {
|
||||
if (typeof key[path[i]] === 'undefined') {
|
||||
key[path[i]] = {}
|
||||
}
|
||||
key = key[path[i]]
|
||||
}
|
||||
key[path[i]] = value
|
||||
}
|
||||
|
||||
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||
function savePreferences () {
|
||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||
State.save(state)
|
||||
update()
|
||||
}
|
||||
|
||||
// Don't write state.saved to file more than once a second
|
||||
@@ -517,44 +533,13 @@ function saveStateThrottled () {
|
||||
if (state.saveStateTimeout) return
|
||||
state.saveStateTimeout = setTimeout(function () {
|
||||
delete state.saveStateTimeout
|
||||
saveState()
|
||||
State.save(state)
|
||||
update()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Write state.saved to the JSON state file
|
||||
function saveState () {
|
||||
console.log('saving state to ' + appConfig.filePath)
|
||||
|
||||
// Clean up, so that we're not saving any pending state
|
||||
var copy = Object.assign({}, state.saved)
|
||||
// Remove torrents pending addition to the list, where we haven't finished
|
||||
// reading the torrent file or file(s) to seed & don't have an infohash
|
||||
copy.torrents = copy.torrents
|
||||
.filter((x) => x.infoHash)
|
||||
.map(function (x) {
|
||||
var torrent = {}
|
||||
for (var key in x) {
|
||||
if (key === 'progress' || key === 'torrentKey') {
|
||||
continue // Don't save progress info or key for the webtorrent process
|
||||
}
|
||||
if (key === 'playStatus' && x.playStatus !== 'unplayable') {
|
||||
continue // Don't save whether a torrent is playing / pending
|
||||
}
|
||||
torrent[key] = x[key]
|
||||
}
|
||||
return torrent
|
||||
})
|
||||
|
||||
appConfig.write(copy, function (err) {
|
||||
if (err) console.error(err)
|
||||
ipcRenderer.send('savedState')
|
||||
})
|
||||
|
||||
// Update right away, don't wait for the state to save
|
||||
update()
|
||||
}
|
||||
|
||||
// Called when the user drag-drops files onto the app
|
||||
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
|
||||
// via any method (drag-drop, drag to app icon, command line)
|
||||
function onOpen (files) {
|
||||
if (!Array.isArray(files)) files = [ files ]
|
||||
|
||||
@@ -605,24 +590,31 @@ function getTorrentSummary (torrentKey) {
|
||||
|
||||
// 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-
|
||||
var instantIoRegex = /^(https:\/\/)?instant\.io\/#/
|
||||
function addTorrent (torrentId) {
|
||||
backToList()
|
||||
var torrentKey = state.nextTorrentKey++
|
||||
var path = state.saved.downloadPath
|
||||
var path = state.saved.prefs.downloadPath
|
||||
if (torrentId.path) {
|
||||
// Use path string instead of W3C File object
|
||||
torrentId = torrentId.path
|
||||
}
|
||||
// Allow a instant.io link to be pasted
|
||||
// TODO: remove this once support is added to webtorrent core
|
||||
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||
}
|
||||
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||
}
|
||||
|
||||
function addSubtitles (files, autoSelect) {
|
||||
// Subtitles are only supported while playing video
|
||||
// Subtitles are only supported when playing video files
|
||||
if (state.playing.type !== 'video') return
|
||||
if (files.length === 0) return
|
||||
|
||||
// Read the files concurrently, then add all resulting subtitle tracks
|
||||
var jobs = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||
parallel(jobs, function (err, tracks) {
|
||||
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||
parallel(tasks, function (err, tracks) {
|
||||
if (err) return onError(err)
|
||||
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
@@ -647,8 +639,9 @@ function addSubtitles (files, autoSelect) {
|
||||
}
|
||||
|
||||
function loadSubtitle (file, cb) {
|
||||
var srtToVtt = require('srt-to-vtt')
|
||||
var concat = require('simple-concat')
|
||||
var LanguageDetect = require('languagedetect')
|
||||
var srtToVtt = require('srt-to-vtt')
|
||||
|
||||
// Read the .SRT or .VTT file, parse it, add subtitle track
|
||||
var filePath = file.path || file
|
||||
@@ -664,12 +657,8 @@ function loadSubtitle (file, cb) {
|
||||
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.
|
||||
// The only way to change cue text position is by modifying the VTT. It is not
|
||||
// possible via CSS.
|
||||
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
|
||||
var track = {
|
||||
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
|
||||
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
||||
language: langDetected,
|
||||
label: langDetected,
|
||||
filePath: filePath
|
||||
@@ -686,6 +675,7 @@ function selectSubtitle (ix) {
|
||||
// Checks whether a language name like "English" or "German" matches the system
|
||||
// language, aka the current locale
|
||||
function isSystemLanguage (language) {
|
||||
var iso639 = require('iso-639-1')
|
||||
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
|
||||
var langIso = iso639.getCode(language) // eg "de" if language is "German"
|
||||
return langIso === osLangISO
|
||||
@@ -728,7 +718,7 @@ function startTorrentingSummary (torrentSummary) {
|
||||
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
|
||||
|
||||
// Use Downloads folder by default
|
||||
var path = s.path || state.saved.downloadPath
|
||||
var path = s.path || state.saved.prefs.downloadPath
|
||||
|
||||
var torrentID
|
||||
if (s.torrentFileName) { // Load torrent file from disk
|
||||
@@ -748,7 +738,9 @@ function startTorrentingSummary (torrentSummary) {
|
||||
|
||||
// Shows the Create Torrent page with options to seed a given file or folder
|
||||
function showCreateTorrent (files) {
|
||||
if (Array.isArray(files)) {
|
||||
// Files will either be an array of file objects, which we can send directly
|
||||
// to the create-torrent screen
|
||||
if (files.length === 0 || typeof files[0] !== 'string') {
|
||||
state.location.go({
|
||||
url: 'create-torrent',
|
||||
files: files
|
||||
@@ -756,13 +748,30 @@ function showCreateTorrent (files) {
|
||||
return
|
||||
}
|
||||
|
||||
var fileOrFolder = files
|
||||
findFilesRecursive(fileOrFolder, showCreateTorrent)
|
||||
// ... or it will be an array of mixed file and folder paths. We have to walk
|
||||
// through all the folders and find the files
|
||||
findFilesRecursive(files, showCreateTorrent)
|
||||
}
|
||||
|
||||
// Recursively finds {name, path, size} for all files in a folder
|
||||
// Calls `cb` on success, calls `onError` on failure
|
||||
function findFilesRecursive (fileOrFolder, cb) {
|
||||
function findFilesRecursive (paths, cb) {
|
||||
if (paths.length > 1) {
|
||||
var numComplete = 0
|
||||
var ret = []
|
||||
paths.forEach(function (path) {
|
||||
findFilesRecursive([path], function (fileObjs) {
|
||||
ret = ret.concat(fileObjs)
|
||||
if (++numComplete === paths.length) {
|
||||
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
|
||||
cb(ret)
|
||||
}
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var fileOrFolder = paths[0]
|
||||
fs.stat(fileOrFolder, function (err, stat) {
|
||||
if (err) return onError(err)
|
||||
|
||||
@@ -780,16 +789,8 @@ function findFilesRecursive (fileOrFolder, cb) {
|
||||
var folderPath = fileOrFolder
|
||||
fs.readdir(folderPath, function (err, fileNames) {
|
||||
if (err) return onError(err)
|
||||
var numComplete = 0
|
||||
var ret = []
|
||||
fileNames.forEach(function (fileName) {
|
||||
findFilesRecursive(path.join(folderPath, fileName), function (fileObjs) {
|
||||
ret = ret.concat(fileObjs)
|
||||
if (++numComplete === fileNames.length) {
|
||||
cb(ret)
|
||||
}
|
||||
})
|
||||
})
|
||||
var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
||||
findFilesRecursive(paths, cb)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -809,6 +810,12 @@ function torrentInfoHash (torrentKey, infoHash) {
|
||||
torrentSummary ? 'existing' : 'new', torrentKey)
|
||||
|
||||
if (!torrentSummary) {
|
||||
// Check if an existing (non-active) torrent has the same info hash
|
||||
if (state.saved.torrents.find((t) => t.infoHash === infoHash)) {
|
||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||
return onError(new Error('Cannot add duplicate torrent'))
|
||||
}
|
||||
|
||||
torrentSummary = {
|
||||
torrentKey: torrentKey,
|
||||
status: 'new'
|
||||
@@ -847,11 +854,17 @@ function torrentMetadata (torrentKey, torrentInfo) {
|
||||
torrentSummary.status = 'downloading'
|
||||
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
||||
torrentSummary.path = torrentInfo.path
|
||||
torrentSummary.files = torrentInfo.files
|
||||
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
||||
// For now, check whether torrentSummary.files has already been set:
|
||||
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
||||
if (!hasDetailedFileInfo) {
|
||||
torrentSummary.files = torrentInfo.files
|
||||
}
|
||||
if (!torrentSummary.selections) {
|
||||
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||
}
|
||||
torrentSummary.defaultPlayFileIndex = pickFileToPlay(torrentInfo.files)
|
||||
update()
|
||||
|
||||
// Save the .torrent file, if it hasn't been saved already
|
||||
@@ -903,7 +916,10 @@ function torrentProgress (progressInfo) {
|
||||
torrentSummary.progress = p
|
||||
})
|
||||
|
||||
checkForSubtitles()
|
||||
// TODO: Find an efficient way to re-enable this line, which allows subtitle
|
||||
// files which are completed after a video starts to play to be added
|
||||
// dynamically to the list of subtitles.
|
||||
// checkForSubtitles()
|
||||
|
||||
update()
|
||||
}
|
||||
@@ -960,23 +976,40 @@ function pickFileToPlay (files) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Opens the video player
|
||||
function playFile (infoHash, index) {
|
||||
state.location.go({
|
||||
url: 'player',
|
||||
onbeforeload: function (cb) {
|
||||
play()
|
||||
openPlayer(infoHash, index, cb)
|
||||
},
|
||||
onbeforeunload: closePlayer
|
||||
}, function (err) {
|
||||
if (err) onError(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Opens the video player to a specific torrent
|
||||
function openPlayer (infoHash, index, cb) {
|
||||
var torrentSummary = getTorrentSummary(infoHash)
|
||||
|
||||
// automatically choose which file in the torrent to play, if necessary
|
||||
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
|
||||
if (index === undefined) index = pickFileToPlay(torrentSummary.files)
|
||||
if (index === undefined) return cb(new errors.UnplayableError())
|
||||
|
||||
// update UI to show pending playback
|
||||
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
||||
// TODO: remove torrentSummary.playStatus
|
||||
torrentSummary.playStatus = 'requested'
|
||||
update()
|
||||
|
||||
var timeout = setTimeout(function () {
|
||||
telemetry.logPlayAttempt('timeout')
|
||||
// TODO: remove torrentSummary.playStatus
|
||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||
sound.play('ERROR')
|
||||
cb(new Error('playback timed out'))
|
||||
cb(new Error('Playback timed out. Try again.'))
|
||||
update()
|
||||
}, 10000) /* give it a few seconds */
|
||||
|
||||
@@ -999,6 +1032,15 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
||||
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||
: 'other'
|
||||
|
||||
// pick up where we left off
|
||||
if (fileSummary.currentTime) {
|
||||
var fraction = fileSummary.currentTime / fileSummary.duration
|
||||
var secondsLeft = fileSummary.duration - fileSummary.currentTime
|
||||
if (fraction < 0.9 && secondsLeft > 10) {
|
||||
state.playing.jumpToTime = fileSummary.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
// if it's audio, parse out the metadata (artist, title, etc)
|
||||
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
||||
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
||||
@@ -1030,22 +1072,41 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
||||
}
|
||||
|
||||
function closePlayer (cb) {
|
||||
console.log('closePlayer')
|
||||
|
||||
// Quit any external players, like Chromecast/Airplay/etc or VLC
|
||||
if (isCasting()) {
|
||||
Cast.close()
|
||||
Cast.stop()
|
||||
}
|
||||
if (state.playing.location === 'vlc') {
|
||||
ipcRenderer.send('vlcQuit')
|
||||
}
|
||||
|
||||
// Save volume (this session only, not in state.saved)
|
||||
state.previousVolume = state.playing.volume
|
||||
|
||||
// Telemetry: track what happens after the user clicks play
|
||||
var result = state.playing.result // 'success' or 'error'
|
||||
if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed
|
||||
else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc
|
||||
else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame
|
||||
else console.error('Unknown state.playing.result', state.playing.result)
|
||||
|
||||
// Reset the window contents back to the home screen
|
||||
state.window.title = config.APP_WINDOW_TITLE
|
||||
state.playing = State.getDefaultPlayState()
|
||||
state.server = null
|
||||
|
||||
// Reset the window size and location back to where it was
|
||||
if (state.window.isFullScreen) {
|
||||
dispatch('toggleFullScreen', false)
|
||||
}
|
||||
restoreBounds()
|
||||
|
||||
// Tell the WebTorrent process to kill the torrent-to-HTTP server
|
||||
ipcRenderer.send('wt-stop-server')
|
||||
|
||||
// Tell the OS we're no longer playing media, laptops allowed to sleep again
|
||||
ipcRenderer.send('unblockPowerSave')
|
||||
ipcRenderer.send('onPlayerClose')
|
||||
|
||||
@@ -1053,7 +1114,7 @@ function closePlayer (cb) {
|
||||
cb()
|
||||
}
|
||||
|
||||
function openFile (infoHash, index) {
|
||||
function openItem (infoHash, index) {
|
||||
var torrentSummary = getTorrentSummary(infoHash)
|
||||
var filePath = path.join(
|
||||
torrentSummary.path,
|
||||
@@ -1076,9 +1137,14 @@ function toggleTorrent (infoHash) {
|
||||
}
|
||||
|
||||
// TODO: use torrentKey, not infoHash
|
||||
function deleteTorrent (infoHash) {
|
||||
function deleteTorrent (infoHash, deleteData) {
|
||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||
|
||||
if (deleteData) {
|
||||
var torrentSummary = getTorrentSummary(infoHash)
|
||||
moveItemToTrash(torrentSummary)
|
||||
}
|
||||
|
||||
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||
if (index > -1) state.saved.torrents.splice(index, 1)
|
||||
saveStateThrottled()
|
||||
@@ -1104,6 +1170,20 @@ function openTorrentContextMenu (infoHash) {
|
||||
var torrentSummary = getTorrentSummary(infoHash)
|
||||
var menu = new electron.remote.Menu()
|
||||
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: 'Remove From List',
|
||||
click: () => deleteTorrent(torrentSummary.infoHash, false)
|
||||
}))
|
||||
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: 'Remove Data File',
|
||||
click: () => deleteTorrent(torrentSummary.infoHash, true)
|
||||
}))
|
||||
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
type: 'separator'
|
||||
}))
|
||||
|
||||
if (torrentSummary.files) {
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
|
||||
@@ -1144,11 +1224,15 @@ function showItemInFolder (torrentSummary) {
|
||||
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
|
||||
}
|
||||
|
||||
function moveItemToTrash (torrentSummary) {
|
||||
ipcRenderer.send('moveItemToTrash', getTorrentPath(torrentSummary))
|
||||
}
|
||||
|
||||
function saveTorrentFileAs (torrentSummary) {
|
||||
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
||||
var opts = {
|
||||
title: 'Save Torrent File',
|
||||
defaultPath: path.join(state.saved.downloadPath, newFileName),
|
||||
defaultPath: path.join(state.saved.prefs.downloadPath, newFileName),
|
||||
filters: [
|
||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
@@ -1218,7 +1302,7 @@ function showDoneNotification (torrent) {
|
||||
})
|
||||
|
||||
notif.onclick = function () {
|
||||
ipcRenderer.send('focusWindow', 'main')
|
||||
ipcRenderer.send('show')
|
||||
}
|
||||
|
||||
sound.play('DONE')
|
||||
@@ -1281,3 +1365,7 @@ function onBlur () {
|
||||
state.window.isFocused = false
|
||||
update()
|
||||
}
|
||||
|
||||
function onVisibilityChange () {
|
||||
state.window.isVisible = !document.webkitHidden
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
var electron = require('electron')
|
||||
var path = require('path')
|
||||
|
||||
var remote = electron.remote
|
||||
|
||||
var config = require('../config')
|
||||
var LocationHistory = require('./lib/location-history')
|
||||
|
||||
module.exports = {
|
||||
getInitialState,
|
||||
getDefaultPlayState,
|
||||
getDefaultSavedState,
|
||||
getPlayingTorrentSummary
|
||||
}
|
||||
|
||||
function getInitialState () {
|
||||
return {
|
||||
/*
|
||||
* Temporary state disappears once the program exits.
|
||||
* It can contain complex objects like open connections, etc.
|
||||
*/
|
||||
client: null, /* the WebTorrent client */
|
||||
server: null, /* local WebTorrent-to-HTTP server */
|
||||
prev: {}, /* used for state diffing in updateElectron() */
|
||||
location: new LocationHistory(),
|
||||
window: {
|
||||
bounds: null, /* {x, y, width, height } */
|
||||
isFocused: true,
|
||||
isFullScreen: false,
|
||||
title: config.APP_WINDOW_TITLE
|
||||
},
|
||||
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
||||
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
|
||||
devices: { /* playback devices like Chromecast and AppleTV */
|
||||
airplay: null, /* airplay client. finds and manages AppleTVs */
|
||||
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
||||
},
|
||||
dock: {
|
||||
badge: 0,
|
||||
progress: 0
|
||||
},
|
||||
modal: null, /* modal popover */
|
||||
errors: [], /* user-facing errors */
|
||||
nextTorrentKey: 1, /* identify torrents for IPC between the main and webtorrent windows */
|
||||
|
||||
/*
|
||||
* Saved state is read from and written to a file every time the app runs.
|
||||
* It should be simple and minimal and must be JSON.
|
||||
* It must never contain absolute paths since we have a portable app.
|
||||
*
|
||||
* Config path:
|
||||
*
|
||||
* OS X ~/Library/Application Support/WebTorrent/config.json
|
||||
* Linux (XDG) $XDG_CONFIG_HOME/WebTorrent/config.json
|
||||
* Linux (Legacy) ~/.config/WebTorrent/config.json
|
||||
* Windows (> Vista) %LOCALAPPDATA%/WebTorrent/config.json
|
||||
* Windows (XP, 2000) %USERPROFILE%/Local Settings/Application Data/WebTorrent/config.json
|
||||
*
|
||||
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
||||
*/
|
||||
saved: {},
|
||||
|
||||
/*
|
||||
* Getters, for convenience
|
||||
*/
|
||||
getPlayingTorrentSummary
|
||||
}
|
||||
}
|
||||
|
||||
/* Whenever we stop playing video or audio, here's what we reset state.playing to */
|
||||
function getDefaultPlayState () {
|
||||
return {
|
||||
infoHash: null, /* the info hash of the torrent we're playing */
|
||||
fileIndex: null, /* the zero-based index within the torrent */
|
||||
location: 'local', /* 'local', 'chromecast', 'airplay' */
|
||||
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
|
||||
currentTime: 0, /* seconds */
|
||||
duration: 1, /* seconds */
|
||||
isPaused: true,
|
||||
isStalled: false,
|
||||
lastTimeUpdate: 0, /* Unix time in ms */
|
||||
mouseStationarySince: 0, /* Unix time in ms */
|
||||
subtitles: {
|
||||
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
||||
selectedIndex: -1, /* current subtitle track */
|
||||
showMenu: false /* popover menu, above the video */
|
||||
},
|
||||
aspectRatio: 0 /* aspect ratio of the video */
|
||||
}
|
||||
}
|
||||
|
||||
/* If the saved state file doesn't exist yet, here's what we use instead */
|
||||
function getDefaultSavedState () {
|
||||
return {
|
||||
version: 1, /* make sure we can upgrade gracefully later */
|
||||
torrents: [
|
||||
{
|
||||
status: 'paused',
|
||||
infoHash: '88594aaacbde40ef3e2510c47374ec0aa396c08e',
|
||||
magnetURI: 'magnet:?xt=urn:btih:88594aaacbde40ef3e2510c47374ec0aa396c08e&dn=bbb_sunflower_1080p_30fps_normal.mp4&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fdistribution.bbb3d.renderfarming.net%2Fvideo%2Fmp4%2Fbbb_sunflower_1080p_30fps_normal.mp4',
|
||||
displayName: 'Big Buck Bunny',
|
||||
posterURL: 'bigBuckBunny.jpg',
|
||||
torrentPath: 'bigBuckBunny.torrent',
|
||||
files: [
|
||||
{
|
||||
length: 276134947,
|
||||
name: 'bbb_sunflower_1080p_30fps_normal.mp4'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
status: 'paused',
|
||||
infoHash: '6a9759bffd5c0af65319979fb7832189f4f3c35d',
|
||||
magnetURI: 'magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d&dn=sintel.mp4&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel-1024-surround.mp4',
|
||||
displayName: 'Sintel',
|
||||
posterURL: 'sintel.jpg',
|
||||
torrentPath: 'sintel.torrent',
|
||||
files: [
|
||||
{
|
||||
length: 129241752,
|
||||
name: 'sintel.mp4'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
status: 'paused',
|
||||
infoHash: '02767050e0be2fd4db9a2ad6c12416ac806ed6ed',
|
||||
magnetURI: 'magnet:?xt=urn:btih:02767050e0be2fd4db9a2ad6c12416ac806ed6ed&dn=tears_of_steel_1080p.webm&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io',
|
||||
displayName: 'Tears of Steel',
|
||||
posterURL: 'tearsOfSteel.jpg',
|
||||
torrentPath: 'tearsOfSteel.torrent',
|
||||
files: [
|
||||
{
|
||||
length: 571346576,
|
||||
name: 'tears_of_steel_1080p.webm'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
status: 'paused',
|
||||
infoHash: '6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5',
|
||||
magnetURI: 'magnet:?xt=urn:btih:6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5&dn=CosmosLaundromatFirstCycle&tr=http%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=http%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fia601508.us.archive.org%2F14%2Fitems%2F&ws=http%3A%2F%2Fia801508.us.archive.org%2F14%2Fitems%2F&ws=https%3A%2F%2Farchive.org%2Fdownload%2F',
|
||||
displayName: 'Cosmos Laundromat (Preview)',
|
||||
posterURL: 'cosmosLaundromat.jpg',
|
||||
torrentPath: 'cosmosLaundromat.torrent',
|
||||
files: [
|
||||
{
|
||||
length: 223580,
|
||||
name: 'Cosmos Laundromat - First Cycle (1080p).gif'
|
||||
},
|
||||
{
|
||||
length: 220087570,
|
||||
name: 'Cosmos Laundromat - First Cycle (1080p).mp4'
|
||||
},
|
||||
{
|
||||
length: 56832560,
|
||||
name: 'Cosmos Laundromat - First Cycle (1080p).ogv'
|
||||
},
|
||||
{
|
||||
length: 3949,
|
||||
name: 'CosmosLaundromat-FirstCycle1080p.en.srt'
|
||||
},
|
||||
{
|
||||
length: 3907,
|
||||
name: 'CosmosLaundromat-FirstCycle1080p.es.srt'
|
||||
},
|
||||
{
|
||||
length: 4119,
|
||||
name: 'CosmosLaundromat-FirstCycle1080p.fr.srt'
|
||||
},
|
||||
{
|
||||
length: 3941,
|
||||
name: 'CosmosLaundromat-FirstCycle1080p.it.srt'
|
||||
},
|
||||
{
|
||||
length: 11264,
|
||||
name: 'CosmosLaundromatFirstCycle_meta.sqlite'
|
||||
},
|
||||
{
|
||||
length: 1204,
|
||||
name: 'CosmosLaundromatFirstCycle_meta.xml'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
status: 'paused',
|
||||
infoHash: '3ba219a8634bf7bae3d848192b2da75ae995589d',
|
||||
magnetURI: 'magnet:?xt=urn:btih:3ba219a8634bf7bae3d848192b2da75ae995589d&dn=The+WIRED+CD+-+Rip.+Sample.+Mash.+Share.&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F',
|
||||
displayName: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
||||
posterURL: 'wired-cd.jpg',
|
||||
torrentPath: 'wired-cd.torrent',
|
||||
files: [
|
||||
{
|
||||
length: 1964275,
|
||||
name: '01 - Beastie Boys - Now Get Busy.mp3'
|
||||
},
|
||||
{
|
||||
length: 3610523,
|
||||
name: '02 - David Byrne - My Fair Lady.mp3'
|
||||
},
|
||||
{
|
||||
length: 2759377,
|
||||
name: '03 - Zap Mama - Wadidyusay.mp3'
|
||||
},
|
||||
{
|
||||
length: 5816537,
|
||||
name: '04 - My Morning Jacket - One Big Holiday.mp3'
|
||||
},
|
||||
{
|
||||
length: 2106421,
|
||||
name: '05 - Spoon - Revenge!.mp3'
|
||||
},
|
||||
{
|
||||
length: 3347550,
|
||||
name: '06 - Gilberto Gil - Oslodum.mp3'
|
||||
},
|
||||
{
|
||||
length: 2107577,
|
||||
name: '07 - Dan The Automator - Relaxation Spa Treatment.mp3'
|
||||
},
|
||||
{
|
||||
length: 3108130,
|
||||
name: '08 - Thievery Corporation - Dc 3000.mp3'
|
||||
},
|
||||
{
|
||||
length: 3051528,
|
||||
name: '09 - Le Tigre - Fake French.mp3'
|
||||
},
|
||||
{
|
||||
length: 3270259,
|
||||
name: '10 - Paul Westerberg - Looking Up In Heaven.mp3'
|
||||
},
|
||||
{
|
||||
length: 3263528,
|
||||
name: '11 - Chuck D - No Meaning No (feat. Fine Arts Militia).mp3'
|
||||
},
|
||||
{
|
||||
length: 6380952,
|
||||
name: '12 - The Rapture - Sister Saviour (Blackstrobe Remix).mp3'
|
||||
},
|
||||
{
|
||||
length: 6550396,
|
||||
name: '13 - Cornelius - Wataridori 2.mp3'
|
||||
},
|
||||
{
|
||||
length: 3034692,
|
||||
name: '14 - DJ Danger Mouse - What U Sittin\' On (feat. Jemini, Cee Lo And Tha Alkaholiks).mp3'
|
||||
},
|
||||
{
|
||||
length: 3854611,
|
||||
name: '15 - DJ Dolores - Oslodum 2004.mp3'
|
||||
},
|
||||
{
|
||||
length: 1762120,
|
||||
name: '16 - Matmos - Action At A Distance.mp3'
|
||||
},
|
||||
{
|
||||
length: 4071,
|
||||
name: 'README.md'
|
||||
},
|
||||
{
|
||||
length: 78163,
|
||||
name: 'poster.jpg'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
downloadPath: config.IS_PORTABLE
|
||||
? path.join(config.CONFIG_PATH, 'Downloads')
|
||||
: remote.app.getPath('downloads')
|
||||
}
|
||||
}
|
||||
|
||||
function getPlayingTorrentSummary () {
|
||||
var infoHash = this.playing.infoHash
|
||||
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
module.exports = App
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
var Header = require('./header')
|
||||
|
||||
var Views = {
|
||||
'home': require('./torrent-list'),
|
||||
'home': require('./home'),
|
||||
'player': require('./player'),
|
||||
'create-torrent': require('./create-torrent-page')
|
||||
'create-torrent': require('./create-torrent'),
|
||||
'preferences': require('./preferences')
|
||||
}
|
||||
|
||||
var Modals = {
|
||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||
'update-available-modal': require('./update-available-modal'),
|
||||
@@ -26,11 +26,8 @@ function App (state) {
|
||||
state.playing.mouseStationarySince !== 0 &&
|
||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||
!state.playing.isPaused &&
|
||||
state.playing.location === 'local'
|
||||
|
||||
// Hide the header on Windows/Linux when in the player
|
||||
// On OSX, the header appears as part of the title bar
|
||||
var hideHeader = process.platform !== 'darwin' && state.location.url() === 'player'
|
||||
state.playing.location === 'local' &&
|
||||
state.playing.playbackRate === 1
|
||||
|
||||
var cls = [
|
||||
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
||||
@@ -39,7 +36,6 @@ function App (state) {
|
||||
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
||||
if (state.window.isFocused) cls.push('is-focused')
|
||||
if (hideControls) cls.push('hide-video-controls')
|
||||
if (hideHeader) cls.push('hide-header')
|
||||
|
||||
return hx`
|
||||
<div class='app ${cls.join(' ')}'>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
module.exports = CreateTorrentPage
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var createTorrent = require('create-torrent')
|
||||
var path = require('path')
|
||||
var prettyBytes = require('prettier-bytes')
|
||||
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function CreateTorrentPage (state) {
|
||||
var info = state.location.current()
|
||||
@@ -17,17 +14,14 @@ function CreateTorrentPage (state) {
|
||||
var files = info.files
|
||||
.filter((f) => !f.name.startsWith('.'))
|
||||
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
||||
if (files.length === 0) return CreateTorrentErrorPage()
|
||||
|
||||
// First, extract the base folder that the files are all in
|
||||
var pathPrefix = info.folderPath
|
||||
if (!pathPrefix) {
|
||||
if (files.length > 0) {
|
||||
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||
pathPrefix = path.dirname(pathPrefix)
|
||||
}
|
||||
} else {
|
||||
pathPrefix = files[0]
|
||||
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||
pathPrefix = path.dirname(pathPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +56,7 @@ function CreateTorrentPage (state) {
|
||||
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
|
||||
|
||||
return hx`
|
||||
<div class='create-torrent-page'>
|
||||
<div class='create-torrent'>
|
||||
<h2>Create torrent ${defaultName}</h2>
|
||||
<p class="torrent-info">
|
||||
${torrentInfo}
|
||||
@@ -133,6 +127,27 @@ function CreateTorrentPage (state) {
|
||||
}
|
||||
}
|
||||
|
||||
function CreateTorrentErrorPage () {
|
||||
return hx`
|
||||
<div class='create-torrent'>
|
||||
<h2>Create torrent</h2>
|
||||
<p class="torrent-info">
|
||||
<p>
|
||||
Sorry, you must select at least one file that is not a hidden file.
|
||||
</p>
|
||||
<p>
|
||||
Hidden files, starting with a . character, are not included.
|
||||
</p>
|
||||
</p>
|
||||
<p class="float-right">
|
||||
<button class='button-flat light' onclick=${dispatcher('back')}>
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
// Finds the longest common prefix
|
||||
function findCommonPrefix (a, b) {
|
||||
for (var i = 0; i < a.length && i < b.length; i++) {
|
||||
@@ -1,16 +1,13 @@
|
||||
module.exports = Header
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var {dispatcher} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function Header (state) {
|
||||
return hx`
|
||||
<div class='header'>
|
||||
${getTitle()}
|
||||
<div class='nav left'>
|
||||
<div class='nav left float-left'>
|
||||
<i.icon.back
|
||||
class=${state.location.hasBack() ? '' : 'disabled'}
|
||||
title='Back'
|
||||
@@ -24,7 +21,7 @@ function Header (state) {
|
||||
chevron_right
|
||||
</i>
|
||||
</div>
|
||||
<div class='nav right'>
|
||||
<div class='nav right float-right'>
|
||||
${getAddButton()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,12 +34,12 @@ function Header (state) {
|
||||
}
|
||||
|
||||
function getAddButton () {
|
||||
if (state.location.url() !== 'player') {
|
||||
if (state.location.url() === 'home') {
|
||||
return hx`
|
||||
<i
|
||||
class='icon add'
|
||||
title='Add torrent'
|
||||
onclick=${dispatcher('showOpenTorrentFile')}>
|
||||
onclick=${dispatcher('openFiles')}>
|
||||
add
|
||||
</i>
|
||||
`
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
module.exports = TorrentList
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
var prettyBytes = require('prettier-bytes')
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
var TorrentSummary = require('../lib/torrent-summary')
|
||||
var TorrentPlayer = require('../lib/torrent-player')
|
||||
var {dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
function TorrentList (state) {
|
||||
var torrentRows = state.saved.torrents.map(
|
||||
(torrentSummary) => renderTorrent(torrentSummary))
|
||||
(torrentSummary) => renderTorrent(torrentSummary)
|
||||
)
|
||||
|
||||
return hx`
|
||||
<div class='torrent-list'>
|
||||
${torrentRows}
|
||||
@@ -20,11 +20,7 @@ function TorrentList (state) {
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
// Renders a torrent in the torrent list
|
||||
// Includes name, download status, play button, background image
|
||||
// May be expanded for additional info, including the list of files inside
|
||||
function renderTorrent (torrentSummary) {
|
||||
// Get ephemeral data (like progress %) directly from the WebTorrent handle
|
||||
var infoHash = torrentSummary.infoHash
|
||||
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||
|
||||
@@ -118,12 +114,7 @@ function TorrentList (state) {
|
||||
var infoHash = torrentSummary.infoHash
|
||||
|
||||
var playIcon, playTooltip, playClass
|
||||
if (torrentSummary.playStatus === 'unplayable') {
|
||||
playIcon = 'play_arrow'
|
||||
playClass = 'disabled'
|
||||
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
|
||||
'View details and click on individual files to open them in another program.'
|
||||
} else if (torrentSummary.playStatus === 'timeout') {
|
||||
if (torrentSummary.playStatus === 'timeout') {
|
||||
playIcon = 'warning'
|
||||
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
||||
} else {
|
||||
@@ -143,6 +134,18 @@ function TorrentList (state) {
|
||||
downloadTooltip = 'Click to start torrenting.'
|
||||
}
|
||||
|
||||
// Do we have a saved position? Show it using a radial progress bar on top
|
||||
// of the play button, unless already showing a spinner there:
|
||||
var positionElem
|
||||
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
||||
var defaultFile = torrentSummary.files &&
|
||||
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
|
||||
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
|
||||
var fraction = defaultFile.currentTime / defaultFile.duration
|
||||
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
|
||||
playClass = 'resume-position'
|
||||
}
|
||||
|
||||
// Only show the play button for torrents that contain playable media
|
||||
var playButton
|
||||
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
||||
@@ -158,6 +161,7 @@ function TorrentList (state) {
|
||||
|
||||
return hx`
|
||||
<div class='buttons'>
|
||||
${positionElem}
|
||||
${playButton}
|
||||
<i.button-round.icon.download
|
||||
class=${torrentSummary.status}
|
||||
@@ -186,11 +190,17 @@ function TorrentList (state) {
|
||||
filesElement = hx`<div class='files warning'>${message}</div>`
|
||||
} else {
|
||||
// We do know the files. List them and show download stats for each one
|
||||
var fileRows = torrentSummary.files.map(
|
||||
(file, index) => renderFileRow(torrentSummary, file, index))
|
||||
var fileRows = torrentSummary.files
|
||||
.map((file, index) => ({ file, index }))
|
||||
.sort(function (a, b) {
|
||||
if (a.file.name < b.file.name) return -1
|
||||
if (b.file.name < a.file.name) return 1
|
||||
return 0
|
||||
})
|
||||
.map((object) => renderFileRow(torrentSummary, object.file, object.index))
|
||||
|
||||
filesElement = hx`
|
||||
<div class='files'>
|
||||
<strong>Files</strong>
|
||||
<table>
|
||||
${fileRows}
|
||||
</table>
|
||||
@@ -208,7 +218,8 @@ function TorrentList (state) {
|
||||
// Show a single torrentSummary file in the details view for a single torrent
|
||||
function renderFileRow (torrentSummary, file, index) {
|
||||
// First, find out how much of the file we've downloaded
|
||||
var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
|
||||
// Are we even torrenting it?
|
||||
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||
var isDone = false // Are we finished torrenting it?
|
||||
var progress = ''
|
||||
if (torrentSummary.progress && torrentSummary.progress.files) {
|
||||
@@ -217,7 +228,14 @@ function TorrentList (state) {
|
||||
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
||||
}
|
||||
|
||||
// Second, render the file as a table row
|
||||
// Second, for media files where we saved our position, show how far we got
|
||||
var positionElem
|
||||
if (file.currentTime) {
|
||||
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
|
||||
positionElem = renderRadialProgressBar(file.currentTime / file.duration)
|
||||
}
|
||||
|
||||
// Finally, render the file as a table row
|
||||
var isPlayable = TorrentPlayer.isPlayable(file)
|
||||
var infoHash = torrentSummary.infoHash
|
||||
var icon
|
||||
@@ -227,23 +245,24 @@ function TorrentList (state) {
|
||||
handleClick = dispatcher('play', infoHash, index)
|
||||
} else {
|
||||
icon = 'description' /* file icon, opens in OS default app */
|
||||
handleClick = dispatcher('openFile', infoHash, index)
|
||||
handleClick = dispatcher('openItem', 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`
|
||||
<tr>
|
||||
<td class='col-icon ${rowClass}' onclick=${handleClick}>
|
||||
<tr onclick=${handleClick}>
|
||||
<td class='col-icon ${rowClass}'>
|
||||
${positionElem}
|
||||
<i class='icon'>${icon}</i>
|
||||
</td>
|
||||
<td class='col-name ${rowClass}' onclick=${handleClick}>
|
||||
<td class='col-name ${rowClass}'>
|
||||
${file.name}
|
||||
</td>
|
||||
<td class='col-progress ${rowClass}' onclick=${handleClick}>
|
||||
<td class='col-progress ${rowClass}'>
|
||||
${isSelected ? progress : ''}
|
||||
</td>
|
||||
<td class='col-size ${rowClass}' onclick=${handleClick}>
|
||||
<td class='col-size ${rowClass}'>
|
||||
${prettyBytes(file.length)}
|
||||
</td>
|
||||
<td class='col-select'
|
||||
@@ -254,3 +273,24 @@ function TorrentList (state) {
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
function renderRadialProgressBar (fraction, cssClass) {
|
||||
var rotation = 360 * fraction
|
||||
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
||||
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
||||
|
||||
return hx`
|
||||
<div class="radial-progress ${cssClass}">
|
||||
<div class="circle">
|
||||
<div class="mask full" style=${transformFill}>
|
||||
<div class="fill" style=${transformFill}></div>
|
||||
</div>
|
||||
<div class="mask half">
|
||||
<div class="fill" style=${transformFill}></div>
|
||||
<div class="fill fix" style=${transformFix}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inset"></div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
module.exports = OpenTorrentAddressModal
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function OpenTorrentAddressModal (state) {
|
||||
return hx`
|
||||
@@ -14,8 +11,8 @@ function OpenTorrentAddressModal (state) {
|
||||
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
|
||||
</p>
|
||||
<p class='float-right'>
|
||||
<button class='button button-flat' onclick=${handleCancel}>CANCEL</button>
|
||||
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
||||
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
|
||||
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
||||
</p>
|
||||
<script>document.querySelector('#add-torrent-url').focus()</script>
|
||||
</div>
|
||||
@@ -30,7 +27,3 @@ function handleOK () {
|
||||
dispatch('exitModal')
|
||||
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
|
||||
}
|
||||
|
||||
function handleCancel () {
|
||||
dispatch('exitModal')
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
module.exports = Player
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var prettyBytes = require('prettier-bytes')
|
||||
var Bitfield = require('bitfield')
|
||||
var prettyBytes = require('prettier-bytes')
|
||||
var zeroFill = require('zero-fill')
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
var TorrentSummary = require('../lib/torrent-summary')
|
||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
@@ -36,7 +34,8 @@ function renderMedia (state) {
|
||||
|
||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||
// Instead, grab the DOM node and play/pause it if necessary
|
||||
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
|
||||
// Get the <video> or <audio> tag
|
||||
var mediaElement = document.querySelector(state.playing.type)
|
||||
if (mediaElement !== null) {
|
||||
if (state.playing.isPaused && !mediaElement.paused) {
|
||||
mediaElement.pause()
|
||||
@@ -48,6 +47,15 @@ function renderMedia (state) {
|
||||
mediaElement.currentTime = state.playing.jumpToTime
|
||||
state.playing.jumpToTime = null
|
||||
}
|
||||
if (state.playing.playbackRate !== mediaElement.playbackRate) {
|
||||
mediaElement.playbackRate = state.playing.playbackRate
|
||||
}
|
||||
// Recover previous volume
|
||||
if (state.previousVolume !== null && isFinite(state.previousVolume)) {
|
||||
mediaElement.volume = state.previousVolume
|
||||
state.previousVolume = null
|
||||
}
|
||||
|
||||
// Set volume
|
||||
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
||||
mediaElement.volume = state.playing.setVolume
|
||||
@@ -55,13 +63,16 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
// Switch to the newly added subtitle track, if available
|
||||
var tracks = mediaElement.textTracks
|
||||
var tracks = mediaElement.textTracks || []
|
||||
for (var j = 0; j < tracks.length; j++) {
|
||||
tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden'
|
||||
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
||||
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
|
||||
}
|
||||
|
||||
state.playing.currentTime = mediaElement.currentTime
|
||||
state.playing.duration = mediaElement.duration
|
||||
// Save video position
|
||||
var file = state.getPlayingFileSummary()
|
||||
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
||||
file.duration = state.playing.duration = mediaElement.duration
|
||||
state.playing.volume = mediaElement.volume
|
||||
}
|
||||
|
||||
@@ -108,7 +119,7 @@ function renderMedia (state) {
|
||||
</div>
|
||||
`
|
||||
|
||||
// As soon as the video loads enough to know the video dimensions, resize the window
|
||||
// As soon as we know the video dimensions, resize the window
|
||||
function onLoadedMetadata (e) {
|
||||
if (state.playing.type !== 'video') return
|
||||
var video = e.target
|
||||
@@ -125,12 +136,15 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
function onCanPlay (e) {
|
||||
var video = e.target
|
||||
if (video.webkitVideoDecodedByteCount > 0 &&
|
||||
video.webkitAudioDecodedByteCount === 0) {
|
||||
var elem = e.target
|
||||
if (state.playing.type === 'video' &&
|
||||
elem.webkitVideoDecodedByteCount === 0) {
|
||||
dispatch('mediaError', 'Video codec unsupported')
|
||||
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
||||
dispatch('mediaError', 'Audio codec unsupported')
|
||||
} else {
|
||||
video.play()
|
||||
dispatch('mediaSuccess')
|
||||
elem.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +164,8 @@ function renderOverlay (state) {
|
||||
} else if (elems.length !== 0) {
|
||||
style = { backgroundImage: cssBackgroundImageDarkGradient() }
|
||||
} else {
|
||||
return /* Video, not audio, and it isn't stalled, so no spinner. No overlay needed. */
|
||||
// Video playing, so no spinner. No overlay needed
|
||||
return
|
||||
}
|
||||
|
||||
return hx`
|
||||
@@ -161,8 +176,7 @@ function renderOverlay (state) {
|
||||
}
|
||||
|
||||
function renderAudioMetadata (state) {
|
||||
var torrentSummary = state.getPlayingTorrentSummary()
|
||||
var fileSummary = torrentSummary.files[state.playing.fileIndex]
|
||||
var fileSummary = state.getPlayingFileSummary()
|
||||
if (!fileSummary.audioInfo) return
|
||||
var info = fileSummary.audioInfo
|
||||
|
||||
@@ -181,15 +195,37 @@ function renderAudioMetadata (state) {
|
||||
track = info.track.no + ' of ' + info.track.of
|
||||
}
|
||||
|
||||
// Show a small info box in the middle of the screen with title/album/artist/etc
|
||||
// Show a small info box in the middle of the screen with title/album/etc
|
||||
var elems = []
|
||||
if (artist) elems.push(hx`<div class='audio-artist'><label>Artist</label>${artist}</div>`)
|
||||
if (album) elems.push(hx`<div class='audio-album'><label>Album</label>${album}</div>`)
|
||||
if (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`)
|
||||
if (artist) {
|
||||
elems.push(hx`
|
||||
<div class='audio-artist'>
|
||||
<label>Artist</label>${artist}
|
||||
</div>
|
||||
`)
|
||||
}
|
||||
if (album) {
|
||||
elems.push(hx`
|
||||
<div class='audio-album'>
|
||||
<label>Album</label>${album}
|
||||
</div>
|
||||
`)
|
||||
}
|
||||
if (track) {
|
||||
elems.push(hx`
|
||||
<div class='audio-track'>
|
||||
<label>Track</label>${track}
|
||||
</div>
|
||||
`)
|
||||
}
|
||||
|
||||
// Align the title with the artist/etc info, if available. Otherwise, center the title
|
||||
// Align the title with the other info, if available. Otherwise, center title
|
||||
var emptyLabel = hx`<label></label>`
|
||||
elems.unshift(hx`<div class='audio-title'>${elems.length ? emptyLabel : undefined}${title}</div>`)
|
||||
elems.unshift(hx`
|
||||
<div class='audio-title'>
|
||||
${elems.length ? emptyLabel : undefined}${title}
|
||||
</div>
|
||||
`)
|
||||
|
||||
return hx`<div class='audio-metadata'>${elems}</div>`
|
||||
}
|
||||
@@ -244,8 +280,10 @@ function renderCastScreen (state) {
|
||||
}
|
||||
|
||||
var isStarting = state.playing.location.endsWith('-pending')
|
||||
var castName = state.playing.castName
|
||||
var castStatus
|
||||
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
|
||||
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
|
||||
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
|
||||
else castStatus = ''
|
||||
|
||||
// Show a nice title image, if possible
|
||||
@@ -264,6 +302,30 @@ function renderCastScreen (state) {
|
||||
`
|
||||
}
|
||||
|
||||
function renderCastOptions (state) {
|
||||
if (!state.devices.castMenu) return
|
||||
|
||||
var {location, devices} = state.devices.castMenu
|
||||
var player = state.devices[location]
|
||||
|
||||
var items = devices.map(function (device, ix) {
|
||||
var isSelected = player.device === device
|
||||
var name = device.name
|
||||
return hx`
|
||||
<li onclick=${dispatcher('selectCastDevice', ix)}>
|
||||
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||
${name}
|
||||
</li>
|
||||
`
|
||||
})
|
||||
|
||||
return hx`
|
||||
<ul.options-list>
|
||||
${items}
|
||||
</ul>
|
||||
`
|
||||
}
|
||||
|
||||
function renderSubtitlesOptions (state) {
|
||||
var subtitles = state.playing.subtitles
|
||||
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||
@@ -272,18 +334,19 @@ function renderSubtitlesOptions (state) {
|
||||
var isSelected = state.playing.subtitles.selectedIndex === ix
|
||||
return hx`
|
||||
<li onclick=${dispatcher('selectSubtitle', ix)}>
|
||||
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||
<i.icon>${'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
||||
${track.label}
|
||||
</li>
|
||||
`
|
||||
})
|
||||
|
||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||
return hx`
|
||||
<ul.subtitles-list>
|
||||
<ul.options-list>
|
||||
${items}
|
||||
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
||||
<i.icon>${noneSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||
<i.icon>${noneClass}</i>
|
||||
None
|
||||
</li>
|
||||
</ul>
|
||||
@@ -292,7 +355,7 @@ function renderSubtitlesOptions (state) {
|
||||
|
||||
function renderPlayerControls (state) {
|
||||
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
||||
? 'disabled'
|
||||
: state.playing.subtitles.selectedIndex >= 0
|
||||
@@ -303,15 +366,27 @@ function renderPlayerControls (state) {
|
||||
hx`
|
||||
<div class='playback-bar'>
|
||||
${renderLoadingBar(state)}
|
||||
<div class='playback-cursor' style=${playbackCursorStyle}></div>
|
||||
<div class='scrub-bar'
|
||||
<div
|
||||
class='playback-cursor'
|
||||
style=${playbackCursorStyle}>
|
||||
</div>
|
||||
<div
|
||||
class='scrub-bar'
|
||||
draggable='true'
|
||||
ondragstart=${handleDragStart}
|
||||
onclick=${handleScrub},
|
||||
ondrag=${handleScrub}></div>
|
||||
ondrag=${handleScrub}>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
hx`
|
||||
<i class='icon fullscreen'
|
||||
<i class='icon play-pause float-left' onclick=${dispatcher('playPause')}>
|
||||
${state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||
</i>
|
||||
`,
|
||||
hx`
|
||||
<i
|
||||
class='icon fullscreen float-right'
|
||||
onclick=${dispatcher('toggleFullScreen')}>
|
||||
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||
</i>
|
||||
@@ -321,125 +396,123 @@ function renderPlayerControls (state) {
|
||||
if (state.playing.type === 'video') {
|
||||
// show closed captions icon
|
||||
elements.push(hx`
|
||||
<i.icon.closed-captions
|
||||
<i.icon.closed-caption.float-right
|
||||
class=${captionsClass}
|
||||
onclick=${handleSubtitles}>
|
||||
closed_captions
|
||||
closed_caption
|
||||
</i>
|
||||
`)
|
||||
}
|
||||
|
||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
||||
var isOnAirplay = state.playing.location.startsWith('airplay')
|
||||
var isOnDlna = state.playing.location.startsWith('dlna')
|
||||
var chromecastClass, chromecastHandler, airplayClass, airplayHandler, dlnaClass, dlnaHandler
|
||||
if (isOnChromecast) {
|
||||
chromecastClass = 'active'
|
||||
dlnaClass = 'disabled'
|
||||
airplayClass = 'disabled'
|
||||
chromecastHandler = dispatcher('closeDevice')
|
||||
airplayHandler = undefined
|
||||
dlnaHandler = undefined
|
||||
} else if (isOnAirplay) {
|
||||
chromecastClass = 'disabled'
|
||||
dlnaClass = 'disabled'
|
||||
airplayClass = 'active'
|
||||
chromecastHandler = undefined
|
||||
airplayHandler = dispatcher('closeDevice')
|
||||
dlnaHandler = undefined
|
||||
} else if (isOnDlna) {
|
||||
chromecastClass = 'disabled'
|
||||
dlnaClass = 'active'
|
||||
airplayClass = 'disabled'
|
||||
chromecastHandler = undefined
|
||||
airplayHandler = undefined
|
||||
dlnaHandler = dispatcher('closeDevice')
|
||||
} else {
|
||||
chromecastClass = ''
|
||||
airplayClass = ''
|
||||
dlnaClass = ''
|
||||
chromecastHandler = dispatcher('openDevice', 'chromecast')
|
||||
airplayHandler = dispatcher('openDevice', 'airplay')
|
||||
dlnaHandler = dispatcher('openDevice', 'dlna')
|
||||
}
|
||||
if (state.devices.chromecast || isOnChromecast) {
|
||||
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
|
||||
elements.push(hx`
|
||||
<i.icon.device
|
||||
class=${chromecastClass}
|
||||
onclick=${chromecastHandler}>
|
||||
${castIcon}
|
||||
</i>
|
||||
`)
|
||||
}
|
||||
if (state.devices.airplay || isOnAirplay) {
|
||||
elements.push(hx`
|
||||
<i.icon.device
|
||||
class=${airplayClass}
|
||||
onclick=${airplayHandler}>
|
||||
airplay
|
||||
</i>
|
||||
`)
|
||||
}
|
||||
if (state.devices.dlna || isOnDlna) {
|
||||
elements.push(hx`
|
||||
<i.icon.device
|
||||
class=${dlnaClass}
|
||||
onclick=${dlnaHandler}>
|
||||
tv
|
||||
</i>
|
||||
`)
|
||||
}
|
||||
var castTypes = ['chromecast', 'airplay', 'dlna']
|
||||
var isCastingAnywhere = castTypes.some(
|
||||
(castType) => state.playing.location.startsWith(castType))
|
||||
|
||||
// Add the cast buttons. Icons for each cast type, connected/disconnected:
|
||||
var buttonIcons = {
|
||||
'chromecast': {true: 'cast_connected', false: 'cast'},
|
||||
'airplay': {true: 'airplay', false: 'airplay'},
|
||||
'dnla': {true: 'tv', false: 'tv'}
|
||||
}
|
||||
castTypes.forEach(function (castType) {
|
||||
// Do we show this button (eg. the Chromecast button) at all?
|
||||
var isCasting = state.playing.location.startsWith(castType)
|
||||
var player = state.devices[castType]
|
||||
if ((!player || player.getDevices().length === 0) && !isCasting) return
|
||||
|
||||
// Show the button. Three options for eg the Chromecast button:
|
||||
var buttonClass, buttonHandler
|
||||
if (isCasting) {
|
||||
// Option 1: we are currently connected to Chromecast. Button stops the cast.
|
||||
buttonClass = 'active'
|
||||
buttonHandler = dispatcher('stopCasting')
|
||||
} else if (isCastingAnywhere) {
|
||||
// Option 2: we are currently connected somewhere else. Button disabled.
|
||||
buttonClass = 'disabled'
|
||||
buttonHandler = undefined
|
||||
} else {
|
||||
// Option 3: we are not connected anywhere. Button opens Chromecast menu.
|
||||
buttonClass = ''
|
||||
buttonHandler = dispatcher('toggleCastMenu', castType)
|
||||
}
|
||||
var buttonIcon = buttonIcons[castType][isCasting]
|
||||
|
||||
// On OSX, the back button is in the title bar of the window; see app.js
|
||||
// On other platforms, we render one over the video on mouseover
|
||||
if (process.platform !== 'darwin') {
|
||||
elements.push(hx`
|
||||
<i.icon.back
|
||||
onclick=${dispatcher('back')}>
|
||||
chevron_left
|
||||
<i.icon.device.float-right
|
||||
class=${buttonClass}
|
||||
onclick=${buttonHandler}>
|
||||
${buttonIcon}
|
||||
</i>
|
||||
`)
|
||||
}
|
||||
})
|
||||
|
||||
// render volume
|
||||
// Render volume slider
|
||||
var volume = state.playing.volume
|
||||
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
|
||||
var volumeStyle = { background: '-webkit-gradient(linear, left top, right top, ' +
|
||||
'color-stop(' + (volume * 100) + '%, #eee), ' +
|
||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
||||
var volumeIcon = 'volume_' + (
|
||||
volume === 0 ? 'off'
|
||||
: volume < 0.3 ? 'mute'
|
||||
: volume < 0.6 ? 'down'
|
||||
: 'up')
|
||||
var volumeStyle = {
|
||||
background: '-webkit-gradient(linear, left top, right top, ' +
|
||||
'color-stop(' + (volume * 100) + '%, #eee), ' +
|
||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
||||
}
|
||||
|
||||
elements.push(hx`
|
||||
<div.volume>
|
||||
<i.icon.volume-icon onmousedown=${handleVolumeMute}>
|
||||
${volumeIcon}
|
||||
</i>
|
||||
<input.volume-slider
|
||||
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
|
||||
onmousedown=${handleVolumeScrub}
|
||||
onmouseup=${handleVolumeScrub}
|
||||
onmousemove=${handleVolumeScrub}
|
||||
style=${volumeStyle}
|
||||
/>
|
||||
<div class='volume float-left'>
|
||||
<i
|
||||
class='icon volume-icon float-left'
|
||||
onmousedown=${handleVolumeMute}>
|
||||
${volumeIcon}
|
||||
</i>
|
||||
<input
|
||||
class='volume-slider float-right'
|
||||
type='range' min='0' max='1' step='0.05'
|
||||
value=${volumeChanging !== false ? volumeChanging : volume}
|
||||
onmousedown=${handleVolumeScrub}
|
||||
onmouseup=${handleVolumeScrub}
|
||||
onmousemove=${handleVolumeScrub}
|
||||
style=${volumeStyle}
|
||||
/>
|
||||
</div>
|
||||
`)
|
||||
|
||||
// Finally, the big button in the center plays or pauses the video
|
||||
// Show video playback progress
|
||||
var currentTimeStr = formatTime(state.playing.currentTime)
|
||||
var durationStr = formatTime(state.playing.duration)
|
||||
elements.push(hx`
|
||||
<i class='icon play-pause' onclick=${dispatcher('playPause')}>
|
||||
${state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||
</i>
|
||||
<span class='time float-left'>
|
||||
${currentTimeStr} / ${durationStr}
|
||||
</span>
|
||||
`)
|
||||
|
||||
// render playback rate
|
||||
if (state.playing.playbackRate !== 1) {
|
||||
elements.push(hx`
|
||||
<span class='rate float-left'>
|
||||
${state.playing.playbackRate}x
|
||||
</span>
|
||||
`)
|
||||
}
|
||||
|
||||
return hx`
|
||||
<div class='player-controls'>
|
||||
<div class='controls'>
|
||||
${elements}
|
||||
${renderCastOptions(state)}
|
||||
${renderSubtitlesOptions(state)}
|
||||
</div>
|
||||
`
|
||||
|
||||
function handleDragStart (e) {
|
||||
// Prevent the cursor from changing, eg to a green + icon on Mac
|
||||
if (e.dataTransfer) {
|
||||
var dt = e.dataTransfer
|
||||
dt.effectAllowed = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
// Handles a click or drag to scrub (jump to another position in the video)
|
||||
function handleScrub (e) {
|
||||
dispatch('mediaMouseMoved')
|
||||
@@ -540,3 +613,18 @@ function cssBackgroundImageDarkGradient () {
|
||||
return 'radial-gradient(circle at center, ' +
|
||||
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
||||
}
|
||||
|
||||
function formatTime (time) {
|
||||
if (typeof time !== 'number' || Number.isNaN(time)) {
|
||||
return '0:00'
|
||||
}
|
||||
|
||||
var hours = Math.floor(time / 3600)
|
||||
var minutes = Math.floor(time % 3600 / 60)
|
||||
if (hours > 0) {
|
||||
minutes = zeroFill(2, minutes)
|
||||
}
|
||||
var seconds = zeroFill(2, Math.floor(time % 60))
|
||||
|
||||
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
|
||||
}
|
||||
|
||||
102
renderer/views/preferences.js
Normal file
102
renderer/views/preferences.js
Normal file
@@ -0,0 +1,102 @@
|
||||
module.exports = Preferences
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
var remote = require('electron').remote
|
||||
var dialog = remote.dialog
|
||||
|
||||
function Preferences (state) {
|
||||
return hx`
|
||||
<div class='preferences'>
|
||||
${renderGeneralSection(state)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderGeneralSection (state) {
|
||||
return renderSection({
|
||||
title: 'General',
|
||||
description: '',
|
||||
icon: 'settings'
|
||||
}, [
|
||||
renderDownloadDirSelector(state)
|
||||
])
|
||||
}
|
||||
|
||||
function renderDownloadDirSelector (state) {
|
||||
return renderFileSelector({
|
||||
label: 'Download Path',
|
||||
description: 'Data from torrents will be saved here',
|
||||
property: 'downloadPath',
|
||||
options: {
|
||||
title: 'Select download directory',
|
||||
properties: [ 'openDirectory' ]
|
||||
}
|
||||
},
|
||||
state.unsaved.prefs.downloadPath,
|
||||
function (filePath) {
|
||||
setStateValue('downloadPath', filePath)
|
||||
})
|
||||
}
|
||||
|
||||
// Renders a prefs section.
|
||||
// - definition should be {icon, title, description}
|
||||
// - controls should be an array of vdom elements
|
||||
function renderSection (definition, controls) {
|
||||
var helpElem = !definition.description ? null : hx`
|
||||
<div class='help text'>
|
||||
<i.icon>help_outline</i>${definition.description}
|
||||
</div>
|
||||
`
|
||||
return hx`
|
||||
<section class='section preferences-panel'>
|
||||
<div class='section-container'>
|
||||
<div class='section-heading'>
|
||||
<i.icon>${definition.icon}</i>${definition.title}
|
||||
</div>
|
||||
${helpElem}
|
||||
<div class='section-body'>
|
||||
${controls}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
// Creates a file chooser
|
||||
// - defition should be {label, description, options}
|
||||
// options are passed to dialog.showOpenDialog
|
||||
// - value should be the current pref, a file or folder path
|
||||
// - callback takes a new file or folder path
|
||||
function renderFileSelector (definition, value, callback) {
|
||||
return hx`
|
||||
<div class='control-group'>
|
||||
<div class='controls'>
|
||||
<label class='control-label'>
|
||||
<div class='preference-title'>${definition.label}</div>
|
||||
<div class='preference-description'>${definition.description}</div>
|
||||
</label>
|
||||
<div class='controls'>
|
||||
<input type='text' class='file-picker-text'
|
||||
id=${definition.property}
|
||||
disabled='disabled'
|
||||
value=${value} />
|
||||
<button class='btn' onclick=${handleClick}>
|
||||
<i.icon>folder_open</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
function handleClick () {
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
||||
if (!Array.isArray(filenames)) return
|
||||
callback(filenames[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setStateValue (property, value) {
|
||||
dispatch('updatePreferences', property, value)
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
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')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function UnsupportedMediaModal (state) {
|
||||
var err = state.modal.error
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
module.exports = UpdateAvailableModal
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function UpdateAvailableModal (state) {
|
||||
return hx`
|
||||
<div class='update-available-modal'>
|
||||
<p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p>
|
||||
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
|
||||
<p>
|
||||
<button class='primary' onclick=${handleOK}>Show Download Page</button>
|
||||
<button class='cancel' onclick=${handleCancel}>Skip This Release</button>
|
||||
<p class='float-right'>
|
||||
<button class='button button-flat' onclick=${handleCancel}>Skip This Release</button>
|
||||
<button class='button button-raised' onclick=${handleOK}>Show Download Page</button>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -28,20 +28,13 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
||||
|
||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||
// client, as explained here: https://webtorrent.io/faq
|
||||
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'
|
||||
}
|
||||
})
|
||||
var client = new WebTorrent()
|
||||
|
||||
// WebTorrent-to-HTTP streaming sever
|
||||
var server = window.server = null
|
||||
var server = null
|
||||
|
||||
// Used for diffing, so we only send progress updates when necessary
|
||||
var prevProgress = window.prevProgress = null
|
||||
var prevProgress = null
|
||||
|
||||
init()
|
||||
|
||||
@@ -70,6 +63,10 @@ function init () {
|
||||
|
||||
ipc.send('ipcReadyWebTorrent')
|
||||
|
||||
window.addEventListener('error', (e) =>
|
||||
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
|
||||
true)
|
||||
|
||||
setInterval(updateTorrentProgress, 1000)
|
||||
}
|
||||
|
||||
@@ -180,7 +177,7 @@ function saveTorrentFile (torrentKey) {
|
||||
}
|
||||
|
||||
// Otherwise, save the .torrent file, under the app config folder
|
||||
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) {
|
||||
fs.mkdir(config.TORRENT_PATH, function (_) {
|
||||
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||
console.log('saved torrent file %s', torrentPath)
|
||||
@@ -193,7 +190,7 @@ function saveTorrentFile (torrentKey) {
|
||||
// Checks whether we've already resolved a given infohash to a torrent file
|
||||
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
||||
function checkIfTorrentFileExists (infoHash, cb) {
|
||||
var torrentPath = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
||||
var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||
fs.exists(torrentPath, function (exists) {
|
||||
cb(torrentPath, exists)
|
||||
})
|
||||
@@ -206,10 +203,10 @@ function generateTorrentPoster (torrentKey) {
|
||||
torrentPoster(torrent, function (err, buf, extension) {
|
||||
if (err) return console.log('error generating poster: %o', err)
|
||||
// save it for next time
|
||||
fs.mkdirp(config.CONFIG_POSTER_PATH, function (err) {
|
||||
fs.mkdirp(config.POSTER_PATH, function (err) {
|
||||
if (err) return console.log('error creating poster dir: %o', err)
|
||||
var posterFileName = torrent.infoHash + extension
|
||||
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, posterFileName)
|
||||
var posterFilePath = path.join(config.POSTER_PATH, posterFileName)
|
||||
fs.writeFile(posterFilePath, buf, function (err) {
|
||||
if (err) return console.log('error saving poster: %o', err)
|
||||
// show the poster
|
||||
@@ -277,7 +274,7 @@ function getTorrentProgress () {
|
||||
function startServer (infoHash, index) {
|
||||
var torrent = client.get(infoHash)
|
||||
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
|
||||
else torrent.on('ready', () => startServerFromReadyTorrent(torrent, index))
|
||||
else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
|
||||
}
|
||||
|
||||
function startServerFromReadyTorrent (torrent, index, cb) {
|
||||
|
||||
BIN
static/PauseThumbnailBarButton.png
Normal file
BIN
static/PauseThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 B |
BIN
static/PlayThumbnailBarButton.png
Normal file
BIN
static/PlayThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 B |
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Reference in New Issue
Block a user