Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d3e26f15a | ||
|
|
8a95895254 | ||
|
|
5d71f9e9c6 | ||
|
|
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 |
@@ -10,7 +10,7 @@
|
|||||||
- Romain Beaumont <romain.rom1@gmail.com>
|
- Romain Beaumont <romain.rom1@gmail.com>
|
||||||
- Dan Flettre <fletd01@yahoo.com>
|
- Dan Flettre <fletd01@yahoo.com>
|
||||||
- Liam Gray <liam.r.gray@gmail.com>
|
- Liam Gray <liam.r.gray@gmail.com>
|
||||||
- grunjol <grunjol@argenteam.net>
|
- grunjol <grunjol@users.noreply.github.com>
|
||||||
- Rémi Jouannet <remijouannet@users.noreply.github.com>
|
- Rémi Jouannet <remijouannet@users.noreply.github.com>
|
||||||
- Evan Miller <miller.evan815@gmail.com>
|
- Evan Miller <miller.evan815@gmail.com>
|
||||||
- Alex <alxmorais8@msn.com>
|
- Alex <alxmorais8@msn.com>
|
||||||
@@ -21,5 +21,6 @@
|
|||||||
- Benjamin Tan <demoneaux@gmail.com>
|
- Benjamin Tan <demoneaux@gmail.com>
|
||||||
- Mathias Rasmussen <mathiasvr@gmail.com>
|
- Mathias Rasmussen <mathiasvr@gmail.com>
|
||||||
- Sergey Bargamon <sergey@bargamon.ru>
|
- Sergey Bargamon <sergey@bargamon.ru>
|
||||||
|
- Thomas Watson Steen <w@tson.dk>
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
61
CHANGELOG.md
61
CHANGELOG.md
@@ -1,15 +1,72 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
|
## v0.7.1 - 2016-06-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change "Step Forward" keyboard shortcut to `Alt+Left`
|
||||||
|
- Change "Step Backward" keyboard shortcut to to `Alt+Right`
|
||||||
|
|
||||||
|
### 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
|
## v0.6.0 - 2016-05-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added Preferences page
|
- Added Preferences page to set Download folder
|
||||||
- Save video position, resume playback from saved position
|
- Save video position, resume playback from saved position
|
||||||
- Add additional video player keyboard shortcuts (#275)
|
- Add additional video player keyboard shortcuts (#275)
|
||||||
- Use `poster.jpg` file as the poster image if available (#558)
|
- Use `poster.jpg` file as the poster image if available (#558)
|
||||||
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
|
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
|
||||||
- Add support for pasting a `instant.io` links (#559)
|
- Add support for pasting `instant.io` links (#559)
|
||||||
- Add announcement feature
|
- Add announcement feature
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -3,49 +3,52 @@
|
|||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var cp = require('child_process')
|
var cp = require('child_process')
|
||||||
|
|
||||||
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen']
|
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path']
|
||||||
var EXECUTABLE_DEPS = ['gh-release', 'standard']
|
var EXECUTABLE_DEPS = ['gh-release', 'standard']
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
// Scans our codebase and package.json for missing or unused dependencies
|
// Scans codebase for missing or unused dependencies. Exits with code 0 on success.
|
||||||
// Process returns 0 on success, prints a message and returns 1 on failure
|
|
||||||
function main () {
|
function main () {
|
||||||
if (process.platform === 'win32') {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsDeps = findJSDeps()
|
var usedDeps = findUsedDeps()
|
||||||
var packageDeps = findPackageDeps()
|
var packageDeps = findPackageDeps()
|
||||||
|
|
||||||
var missingDeps = jsDeps.filter((dep) =>
|
var missingDeps = usedDeps.filter(
|
||||||
packageDeps.indexOf(dep) < 0 &&
|
(dep) => !packageDeps.includes(dep) && !BUILT_IN_DEPS.includes(dep)
|
||||||
BUILT_IN_DEPS.indexOf(dep) < 0)
|
)
|
||||||
var unusedDeps = packageDeps.filter((dep) =>
|
var unusedDeps = packageDeps.filter(
|
||||||
jsDeps.indexOf(dep) < 0 &&
|
(dep) => !usedDeps.includes(dep) && !EXECUTABLE_DEPS.includes(dep)
|
||||||
EXECUTABLE_DEPS.indexOf(dep) < 0)
|
)
|
||||||
|
|
||||||
if (missingDeps.length > 0) console.log('Missing package dependencies: ' + missingDeps)
|
if (missingDeps.length > 0) {
|
||||||
if (unusedDeps.length > 0) console.log('Unused package dependencies: ' + unusedDeps)
|
console.error('Missing package dependencies: ' + missingDeps)
|
||||||
|
}
|
||||||
if (missingDeps.length + unusedDeps.length > 0) process.exit(1)
|
if (unusedDeps.length > 0) {
|
||||||
|
console.error('Unused package dependencies: ' + unusedDeps)
|
||||||
console.log('Lookin good!')
|
}
|
||||||
|
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 () {
|
function findPackageDeps () {
|
||||||
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
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 devDeps = Object.keys(pkg.devDependencies)
|
||||||
var optionalDeps = Object.keys(pkg.optionalDependencies)
|
var optionalDeps = Object.keys(pkg.optionalDependencies)
|
||||||
|
|
||||||
return [].concat(requiredDeps, devDeps, optionalDeps)
|
return [].concat(deps, devDeps, optionalDeps)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds all dependencies required() in the code
|
// Finds all dependencies that used with `require()`
|
||||||
function findJSDeps () {
|
function findUsedDeps () {
|
||||||
var stdout = cp.execSync('./bin/list-deps.sh')
|
var stdout = cp.execSync('./bin/list-deps.sh')
|
||||||
return stdout.toString().trim().split('\n')
|
return stdout.toString().trim().split('\n')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ var path = require('path')
|
|||||||
|
|
||||||
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
|
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
|
||||||
child.on('close', function (code) {
|
child.on('close', function (code) {
|
||||||
process.exit(code)
|
process.exitCode = code
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,5 @@
|
|||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var open = require('open')
|
var open = require('open')
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
var configPath = path.join(config.CONFIG_PATH, 'config.json')
|
open(config.CONFIG_PATH)
|
||||||
open(configPath)
|
|
||||||
|
|||||||
@@ -327,11 +327,11 @@ function buildDarwin (cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dmg = appDmg(dmgOpts)
|
var dmg = appDmg(dmgOpts)
|
||||||
dmg.on('error', cb)
|
dmg.once('error', cb)
|
||||||
dmg.on('progress', function (info) {
|
dmg.on('progress', function (info) {
|
||||||
if (info.type === 'step-begin') console.log(info.title + '...')
|
if (info.type === 'step-begin') console.log(info.title + '...')
|
||||||
})
|
})
|
||||||
dmg.on('finish', function (info) {
|
dmg.once('finish', function (info) {
|
||||||
console.log('OS X: Created dmg.')
|
console.log('OS X: Created dmg.')
|
||||||
cb(null)
|
cb(null)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
npm run update-authors
|
||||||
git diff --exit-code
|
git diff --exit-code
|
||||||
npm run package -- --sign
|
npm run package -- --sign
|
||||||
git push
|
git push
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
git pull
|
git pull
|
||||||
npm run update-authors
|
|
||||||
git diff --exit-code
|
|
||||||
rm -rf node_modules/
|
rm -rf node_modules/
|
||||||
npm install
|
npm install
|
||||||
npm dedupe
|
npm dedupe
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ while (<>) {
|
|||||||
next if $seen{$_};
|
next if $seen{$_};
|
||||||
next if /<support\@greenkeeper.io>/;
|
next if /<support\@greenkeeper.io>/;
|
||||||
next if /<ungoldman\@gmail.com>/;
|
next if /<ungoldman\@gmail.com>/;
|
||||||
next if /<grunjol\@users.noreply.github.com>/;
|
|
||||||
next if /<dc\@DCs-MacBook.local>/;
|
next if /<dc\@DCs-MacBook.local>/;
|
||||||
next if /<rolandoguedes\@gmail.com>/;
|
next if /<rolandoguedes\@gmail.com>/;
|
||||||
$seen{$_} = push @authors, "- ", $_;
|
$seen{$_} = push @authors, "- ", $_;
|
||||||
|
|||||||
50
config.js
50
config.js
@@ -24,11 +24,39 @@ module.exports = {
|
|||||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||||
|
|
||||||
CONFIG_PATH: getConfigPath(),
|
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 */,
|
DELAYED_INIT: 3000 /* 3 seconds */,
|
||||||
|
|
||||||
|
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
|
||||||
|
|
||||||
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
||||||
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
||||||
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
||||||
@@ -38,8 +66,10 @@ module.exports = {
|
|||||||
IS_PORTABLE: isPortable(),
|
IS_PORTABLE: isPortable(),
|
||||||
IS_PRODUCTION: isProduction(),
|
IS_PRODUCTION: isProduction(),
|
||||||
|
|
||||||
|
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
ROOT_PATH: __dirname,
|
ROOT_PATH: __dirname,
|
||||||
STATIC_PATH: path.join(__dirname, 'static'),
|
STATIC_PATH: path.join(__dirname, 'static'),
|
||||||
|
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||||
|
|
||||||
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
||||||
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
|
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
|
||||||
@@ -57,6 +87,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 () {
|
function isPortable () {
|
||||||
try {
|
try {
|
||||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
var get = require('simple-get')
|
|
||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
@@ -12,27 +11,47 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
|||||||
'?version=' + config.APP_VERSION +
|
'?version=' + config.APP_VERSION +
|
||||||
'&platform=' + process.platform
|
'&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 () {
|
function init () {
|
||||||
get.concat(ANNOUNCEMENT_URL, function (err, res, data) {
|
var get = require('simple-get')
|
||||||
if (err) return log('failed to retrieve remote message')
|
get.concat(ANNOUNCEMENT_URL, onResponse)
|
||||||
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 () {})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
uninstall
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
function install () {
|
function install () {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
@@ -35,11 +34,11 @@ function installDarwin () {
|
|||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
|
|
||||||
// On OS X, only protocols that are listed in Info.plist can be set as the default
|
// On OS X, only protocols that are listed in `Info.plist` can be set as the
|
||||||
// handler at runtime.
|
// default handler at runtime.
|
||||||
app.setAsDefaultProtocolClient('magnet')
|
app.setAsDefaultProtocolClient('magnet')
|
||||||
|
|
||||||
// File handlers are registered in the Info.plist.
|
// File handlers are defined in `Info.plist`.
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstallDarwin () {}
|
function uninstallDarwin () {}
|
||||||
@@ -55,10 +54,22 @@ function installWin32 () {
|
|||||||
|
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
|
|
||||||
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
var iconPath = path.join(
|
||||||
|
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
|
||||||
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
|
)
|
||||||
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND)
|
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:
|
* To add a protocol handler, the following keys must be added to the Windows registry:
|
||||||
@@ -265,7 +276,9 @@ function installLinux () {
|
|||||||
installIconFile()
|
installIconFile()
|
||||||
|
|
||||||
function installDesktopFile () {
|
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)
|
fs.readFile(templatePath, 'utf8', writeDesktopFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ var ipcMain = electron.ipcMain
|
|||||||
var announcement = require('./announcement')
|
var announcement = require('./announcement')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var crashReporter = require('../crash-reporter')
|
var crashReporter = require('../crash-reporter')
|
||||||
|
var dialog = require('./dialog')
|
||||||
|
var dock = require('./dock')
|
||||||
var handlers = require('./handlers')
|
var handlers = require('./handlers')
|
||||||
var ipc = require('./ipc')
|
var ipc = require('./ipc')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var shortcuts = require('./shortcuts')
|
|
||||||
var squirrelWin32 = require('./squirrel-win32')
|
var squirrelWin32 = require('./squirrel-win32')
|
||||||
var tray = require('./tray')
|
var tray = require('./tray')
|
||||||
var updater = require('./updater')
|
var updater = require('./updater')
|
||||||
@@ -54,23 +55,22 @@ function init () {
|
|||||||
|
|
||||||
ipc.init()
|
ipc.init()
|
||||||
|
|
||||||
app.on('will-finish-launching', function () {
|
app.once('will-finish-launching', function () {
|
||||||
crashReporter.init()
|
crashReporter.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('ready', function () {
|
app.on('ready', function () {
|
||||||
isReady = true
|
isReady = true
|
||||||
|
|
||||||
windows.createMainWindow()
|
windows.main.init()
|
||||||
windows.createWebTorrentHiddenWindow()
|
windows.webtorrent.init()
|
||||||
menu.init()
|
menu.init()
|
||||||
shortcuts.init()
|
|
||||||
|
|
||||||
// To keep app startup fast, some code is delayed.
|
// To keep app startup fast, some code is delayed.
|
||||||
setTimeout(delayedInit, config.DELAYED_INIT)
|
setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('ipcReady', function () {
|
app.once('ipcReady', function () {
|
||||||
log('Command line args:', argv)
|
log('Command line args:', argv)
|
||||||
processArgv(argv)
|
processArgv(argv)
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
@@ -81,20 +81,21 @@ function init () {
|
|||||||
|
|
||||||
app.isQuitting = true
|
app.isQuitting = true
|
||||||
e.preventDefault()
|
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())
|
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 () {
|
app.on('activate', function () {
|
||||||
if (isReady) windows.createMainWindow()
|
if (isReady) windows.main.show()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function delayedInit () {
|
function delayedInit () {
|
||||||
announcement.init()
|
announcement.init()
|
||||||
tray.init()
|
dock.init()
|
||||||
handlers.install()
|
handlers.install()
|
||||||
|
tray.init()
|
||||||
updater.init()
|
updater.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,13 +103,12 @@ function onOpen (e, torrentId) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (app.ipcReady) {
|
if (app.ipcReady) {
|
||||||
windows.main.send('dispatch', 'onOpen', torrentId)
|
// Magnet links opened from Chrome won't focus the app without a setTimeout.
|
||||||
// Magnet links opened from Chrome won't focus the app without a setTimeout. The
|
// The confirmation dialog Chrome shows causes Chrome to steal back the focus.
|
||||||
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
|
|
||||||
// Electron issue: https://github.com/atom/electron/issues/4338
|
// Electron issue: https://github.com/atom/electron/issues/4338
|
||||||
setTimeout(function () {
|
setTimeout(() => windows.main.show(), 100)
|
||||||
windows.focusWindow(windows.main)
|
|
||||||
}, 100)
|
processArgv([ torrentId ])
|
||||||
} else {
|
} else {
|
||||||
argv.push(torrentId)
|
argv.push(torrentId)
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ function onAppOpen (newArgv) {
|
|||||||
|
|
||||||
if (app.ipcReady) {
|
if (app.ipcReady) {
|
||||||
log('Second app instance opened, but was prevented:', newArgv)
|
log('Second app instance opened, but was prevented:', newArgv)
|
||||||
windows.focusWindow(windows.main)
|
windows.main.show()
|
||||||
|
|
||||||
processArgv(newArgv)
|
processArgv(newArgv)
|
||||||
} else {
|
} else {
|
||||||
@@ -132,18 +132,22 @@ function sliceArgv (argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function processArgv (argv) {
|
function processArgv (argv) {
|
||||||
|
var torrentIds = []
|
||||||
argv.forEach(function (arg) {
|
argv.forEach(function (arg) {
|
||||||
if (arg === '-n') {
|
if (arg === '-n') {
|
||||||
menu.showOpenSeedFiles()
|
dialog.openSeedDirectory()
|
||||||
} else if (arg === '-o') {
|
} else if (arg === '-o') {
|
||||||
menu.showOpenTorrentFile()
|
dialog.openTorrentFile()
|
||||||
} else if (arg === '-u') {
|
} else if (arg === '-u') {
|
||||||
menu.showOpenTorrentAddress()
|
dialog.openTorrentAddress()
|
||||||
} else if (arg.startsWith('-psn')) {
|
} else if (arg.startsWith('-psn')) {
|
||||||
// Ignore OS X launchd "process serial number" argument
|
// Ignore OS X launchd "process serial number" argument
|
||||||
// More: https://github.com/feross/webtorrent-desktop/issues/214
|
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
||||||
} else {
|
} else {
|
||||||
windows.main.send('dispatch', 'onOpen', arg)
|
torrentIds.push(arg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (torrentIds.length > 0) {
|
||||||
|
windows.main.dispatch('onOpen', torrentIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
214
main/ipc.js
214
main/ipc.js
@@ -5,31 +5,32 @@ module.exports = {
|
|||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
var ipcMain = electron.ipcMain
|
|
||||||
|
|
||||||
|
var dialog = require('./dialog')
|
||||||
|
var dock = require('./dock')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var windows = require('./windows')
|
var powerSaveBlocker = require('./power-save-blocker')
|
||||||
|
var shell = require('./shell')
|
||||||
var shortcuts = require('./shortcuts')
|
var shortcuts = require('./shortcuts')
|
||||||
var vlc = require('./vlc')
|
var vlc = require('./vlc')
|
||||||
|
var windows = require('./windows')
|
||||||
|
|
||||||
// has to be a number, not a boolean, and undefined throws an error
|
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||||
var powerSaveBlockerId = 0
|
|
||||||
|
|
||||||
// messages from the main process, to be sent once the WebTorrent process starts
|
|
||||||
var messageQueueMainToWebTorrent = []
|
var messageQueueMainToWebTorrent = []
|
||||||
|
|
||||||
// holds a ChildProcess while we're playing a video in VLC, null otherwise
|
// holds a ChildProcess while we're playing a video in VLC, null otherwise
|
||||||
var vlcProcess
|
var vlcProcess
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
ipcMain.on('ipcReady', function (e) {
|
var ipc = electron.ipcMain
|
||||||
windows.main.show()
|
|
||||||
|
ipc.once('ipcReady', function (e) {
|
||||||
app.ipcReady = true
|
app.ipcReady = true
|
||||||
app.emit('ipcReady')
|
app.emit('ipcReady')
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('ipcReadyWebTorrent', function (e) {
|
ipc.once('ipcReadyWebTorrent', function (e) {
|
||||||
app.ipcReadyWebTorrent = true
|
app.ipcReadyWebTorrent = true
|
||||||
log('sending %d queued messages from the main win to the webtorrent window',
|
log('sending %d queued messages from the main win to the webtorrent window',
|
||||||
messageQueueMainToWebTorrent.length)
|
messageQueueMainToWebTorrent.length)
|
||||||
@@ -39,113 +40,112 @@ function init () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
|
/**
|
||||||
|
* Dialog
|
||||||
|
*/
|
||||||
|
|
||||||
ipcMain.on('setBounds', function (e, bounds, maximize) {
|
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
|
||||||
setBounds(bounds, maximize)
|
ipc.on('openFiles', () => dialog.openFiles())
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('setAspectRatio', function (e, aspectRatio) {
|
/**
|
||||||
setAspectRatio(aspectRatio)
|
* Dock
|
||||||
})
|
*/
|
||||||
|
|
||||||
ipcMain.on('setBadge', function (e, text) {
|
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
|
||||||
setBadge(text)
|
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('setProgress', function (e, progress) {
|
/**
|
||||||
setProgress(progress)
|
* Events
|
||||||
})
|
*/
|
||||||
|
|
||||||
ipcMain.on('toggleFullScreen', function (e, flag) {
|
ipc.on('onPlayerOpen', function () {
|
||||||
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 () {
|
|
||||||
menu.onPlayerOpen()
|
menu.onPlayerOpen()
|
||||||
shortcuts.onPlayerOpen()
|
shortcuts.onPlayerOpen()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('onPlayerClose', function () {
|
ipc.on('onPlayerClose', function () {
|
||||||
menu.onPlayerClose()
|
menu.onPlayerClose()
|
||||||
shortcuts.onPlayerOpen()
|
shortcuts.onPlayerOpen()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('focusWindow', function (e, windowName) {
|
/**
|
||||||
windows.focusWindow(windows[windowName])
|
* Power Save Blocker
|
||||||
})
|
*/
|
||||||
|
|
||||||
ipcMain.on('downloadFinished', function (e, filePath) {
|
ipc.on('blockPowerSave', () => powerSaveBlocker.start())
|
||||||
if (app.dock) {
|
ipc.on('unblockPowerSave', () => powerSaveBlocker.stop())
|
||||||
// Bounces the Downloads stack if the filePath is inside the Downloads folder.
|
|
||||||
app.dock.downloadFinished(filePath)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('checkForVLC', function (e) {
|
/**
|
||||||
|
* Shell
|
||||||
|
*/
|
||||||
|
|
||||||
|
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
||||||
|
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...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) {
|
vlc.checkForVLC(function (isInstalled) {
|
||||||
windows.main.send('checkForVLC', 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]
|
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) {
|
vlc.spawn(args, function (err, proc) {
|
||||||
if (err) return windows.main.send('dispatch', 'vlcNotFound')
|
if (err) return windows.main.dispatch('vlcNotFound')
|
||||||
vlcProcess = proc
|
vlcProcess = proc
|
||||||
|
|
||||||
// If it works, close the modal after a second
|
// If it works, close the modal after a second
|
||||||
var closeModalTimeout = setTimeout(() =>
|
var closeModalTimeout = setTimeout(() =>
|
||||||
windows.main.send('dispatch', 'exitModal'), 1000)
|
windows.main.dispatch('exitModal'), 1000)
|
||||||
|
|
||||||
vlcProcess.on('close', function (code) {
|
vlcProcess.on('close', function (code) {
|
||||||
clearTimeout(closeModalTimeout)
|
clearTimeout(closeModalTimeout)
|
||||||
if (!vlcProcess) return // Killed
|
if (!vlcProcess) return // Killed
|
||||||
console.log('VLC exited with code ', code)
|
log('VLC exited with code ', code)
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
windows.main.send('dispatch', 'backToList')
|
windows.main.dispatch('backToList')
|
||||||
} else {
|
} else {
|
||||||
windows.main.send('dispatch', 'vlcNotFound')
|
windows.main.dispatch('vlcNotFound')
|
||||||
}
|
}
|
||||||
vlcProcess = null
|
vlcProcess = null
|
||||||
})
|
})
|
||||||
|
|
||||||
vlcProcess.on('error', function (e) {
|
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
|
if (!vlcProcess) return
|
||||||
console.log('Killing VLC, pid ' + vlcProcess.pid)
|
log('Killing VLC, pid ' + vlcProcess.pid)
|
||||||
vlcProcess.kill('SIGKILL') // kill -9
|
vlcProcess.kill('SIGKILL') // kill -9
|
||||||
vlcProcess = null
|
vlcProcess = null
|
||||||
})
|
})
|
||||||
|
|
||||||
// Capture all events
|
// Capture all events
|
||||||
var oldEmit = ipcMain.emit
|
var oldEmit = ipc.emit
|
||||||
ipcMain.emit = function (name, e, ...args) {
|
ipc.emit = function (name, e, ...args) {
|
||||||
// Relay messages between the main window and the WebTorrent hidden window
|
// Relay messages between the main window and the WebTorrent hidden window
|
||||||
if (name.startsWith('wt-') && !app.isQuitting) {
|
if (name.startsWith('wt-') && !app.isQuitting) {
|
||||||
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
|
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
|
||||||
@@ -168,82 +168,6 @@ function init () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Emit all other events normally
|
// Emit all other events normally
|
||||||
oldEmit.call(ipcMain, name, e, ...args)
|
oldEmit.call(ipc, 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ module.exports.error = error
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
@@ -18,7 +17,7 @@ function log (...args) {
|
|||||||
if (app.ipcReady) {
|
if (app.ipcReady) {
|
||||||
windows.main.send('log', ...args)
|
windows.main.send('log', ...args)
|
||||||
} else {
|
} 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) {
|
if (app.ipcReady) {
|
||||||
windows.main.send('error', ...args)
|
windows.main.send('error', ...args)
|
||||||
} else {
|
} else {
|
||||||
app.on('ipcReady', () => windows.main.send('error', ...args))
|
app.once('ipcReady', () => windows.main.send('error', ...args))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
312
main/menu.js
312
main/menu.js
@@ -2,15 +2,10 @@ module.exports = {
|
|||||||
init,
|
init,
|
||||||
onPlayerClose,
|
onPlayerClose,
|
||||||
onPlayerOpen,
|
onPlayerOpen,
|
||||||
|
onToggleAlwaysOnTop,
|
||||||
onToggleFullScreen,
|
onToggleFullScreen,
|
||||||
onWindowHide,
|
onWindowBlur,
|
||||||
onWindowShow,
|
onWindowFocus
|
||||||
|
|
||||||
// TODO: move these out of menu.js -- they don't belong here
|
|
||||||
showOpenSeedFiles,
|
|
||||||
showOpenTorrentAddress,
|
|
||||||
showOpenTorrentFile,
|
|
||||||
toggleFullScreen
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -18,207 +13,67 @@ var electron = require('electron')
|
|||||||
var app = electron.app
|
var app = electron.app
|
||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var log = require('./log')
|
var dialog = require('./dialog')
|
||||||
|
var shell = require('./shell')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
var appMenu
|
var menu
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
|
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||||
electron.Menu.setApplicationMenu(appMenu)
|
electron.Menu.setApplicationMenu(menu)
|
||||||
|
|
||||||
if (app.dock) {
|
|
||||||
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
|
|
||||||
app.dock.setMenu(dockMenu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 toggleDevTools () {
|
|
||||||
log('toggleDevTools')
|
|
||||||
if (windows.main) {
|
|
||||||
windows.main.toggleDevTools()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWebTorrentWindow () {
|
|
||||||
log('showWebTorrentWindow')
|
|
||||||
windows.webtorrent.show()
|
|
||||||
windows.webtorrent.webContents.openDevTools({ detach: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
function playPause () {
|
|
||||||
if (windows.main) {
|
|
||||||
windows.main.send('dispatch', 'playPause')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function increaseVolume () {
|
|
||||||
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 skipForward () {
|
|
||||||
if (windows.main) {
|
|
||||||
windows.main.send('dispatch', 'skip', 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function skipBack () {
|
|
||||||
if (windows.main) {
|
|
||||||
windows.main.send('dispatch', 'skip', -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function increasePlaybackRate () {
|
|
||||||
if (windows.main) {
|
|
||||||
windows.main.send('dispatch', 'changePlaybackRate', 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decreasePlaybackRate () {
|
|
||||||
if (windows.main) {
|
|
||||||
windows.main.send('dispatch', 'changePlaybackRate', -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the preferences window
|
|
||||||
function showPreferences () {
|
|
||||||
windows.main.send('dispatch', 'preferences')
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowShow () {
|
|
||||||
log('onWindowShow')
|
|
||||||
getMenuItem('Full Screen').enabled = true
|
|
||||||
getMenuItem('Float on Top').enabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowHide () {
|
|
||||||
log('onWindowHide')
|
|
||||||
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
|
|
||||||
getMenuItem('Step Forward').enabled = true
|
|
||||||
getMenuItem('Step Backward').enabled = true
|
|
||||||
getMenuItem('Increase Speed').enabled = true
|
|
||||||
getMenuItem('Decrease Speed').enabled = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function onPlayerClose () {
|
||||||
log('onPlayerClose')
|
|
||||||
getMenuItem('Play/Pause').enabled = false
|
getMenuItem('Play/Pause').enabled = false
|
||||||
getMenuItem('Increase Volume').enabled = false
|
getMenuItem('Increase Volume').enabled = false
|
||||||
getMenuItem('Decrease Volume').enabled = false
|
getMenuItem('Decrease Volume').enabled = false
|
||||||
getMenuItem('Add Subtitles File...').enabled = false
|
|
||||||
getMenuItem('Step Forward').enabled = false
|
getMenuItem('Step Forward').enabled = false
|
||||||
getMenuItem('Step Backward').enabled = false
|
getMenuItem('Step Backward').enabled = false
|
||||||
getMenuItem('Increase Speed').enabled = false
|
getMenuItem('Increase Speed').enabled = false
|
||||||
getMenuItem('Decrease Speed').enabled = false
|
getMenuItem('Decrease Speed').enabled = false
|
||||||
|
getMenuItem('Add Subtitles File...').enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleFullScreen (isFullScreen) {
|
function onPlayerOpen () {
|
||||||
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen()
|
getMenuItem('Play/Pause').enabled = true
|
||||||
windows.main.setMenuBarVisibility(!isFullScreen)
|
getMenuItem('Increase Volume').enabled = true
|
||||||
getMenuItem('Full Screen').checked = isFullScreen
|
getMenuItem('Decrease Volume').enabled = true
|
||||||
windows.main.send('fullscreenChanged', isFullScreen)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function onToggleAlwaysOnTop (flag) {
|
||||||
|
getMenuItem('Float on Top').checked = flag
|
||||||
|
}
|
||||||
|
|
||||||
|
function onToggleFullScreen (flag) {
|
||||||
|
getMenuItem('Full Screen').checked = flag
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowBlur () {
|
||||||
|
getMenuItem('Full Screen').enabled = false
|
||||||
|
getMenuItem('Float on Top').enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowFocus () {
|
||||||
|
getMenuItem('Full Screen').enabled = true
|
||||||
|
getMenuItem('Float on Top').enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMenuItem (label) {
|
function getMenuItem (label) {
|
||||||
for (var i = 0; i < appMenu.items.length; i++) {
|
for (var i = 0; i < menu.items.length; i++) {
|
||||||
var menuItem = appMenu.items[i].submenu.items.find(function (item) {
|
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||||
return item.label === label
|
return item.label === label
|
||||||
})
|
})
|
||||||
if (menuItem) return menuItem
|
if (menuItem) return menuItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompts the user for a file, then creates a torrent. Only allows a single file
|
function getMenuTemplate () {
|
||||||
// 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 () {
|
|
||||||
var template = [
|
var template = [
|
||||||
{
|
{
|
||||||
label: 'File',
|
label: 'File',
|
||||||
@@ -228,17 +83,17 @@ function getAppMenuTemplate () {
|
|||||||
? 'Create New Torrent...'
|
? 'Create New Torrent...'
|
||||||
: 'Create New Torrent from Folder...',
|
: 'Create New Torrent from Folder...',
|
||||||
accelerator: 'CmdOrCtrl+N',
|
accelerator: 'CmdOrCtrl+N',
|
||||||
click: showOpenSeedFiles
|
click: () => dialog.openSeedDirectory()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent File...',
|
label: 'Open Torrent File...',
|
||||||
accelerator: 'CmdOrCtrl+O',
|
accelerator: 'CmdOrCtrl+O',
|
||||||
click: showOpenTorrentFile
|
click: () => dialog.openTorrentFile()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent Address...',
|
label: 'Open Torrent Address...',
|
||||||
accelerator: 'CmdOrCtrl+U',
|
accelerator: 'CmdOrCtrl+U',
|
||||||
click: showOpenTorrentAddress
|
click: () => dialog.openTorrentAddress()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
@@ -281,7 +136,7 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
label: 'Preferences',
|
label: 'Preferences',
|
||||||
accelerator: 'CmdOrCtrl+,',
|
accelerator: 'CmdOrCtrl+,',
|
||||||
click: () => showPreferences()
|
click: () => windows.main.dispatch('preferences')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -294,12 +149,20 @@ function getAppMenuTemplate () {
|
|||||||
accelerator: process.platform === 'darwin'
|
accelerator: process.platform === 'darwin'
|
||||||
? 'Ctrl+Command+F'
|
? 'Ctrl+Command+F'
|
||||||
: 'F11',
|
: 'F11',
|
||||||
click: () => toggleFullScreen()
|
click: () => windows.main.toggleFullScreen()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Float on Top',
|
label: 'Float on Top',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
click: () => toggleFloatOnTop()
|
click: () => windows.main.toggleAlwaysOnTop()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Go Back',
|
||||||
|
accelerator: 'Esc',
|
||||||
|
click: () => windows.main.dispatch('escapeBack')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
@@ -312,14 +175,14 @@ function getAppMenuTemplate () {
|
|||||||
accelerator: process.platform === 'darwin'
|
accelerator: process.platform === 'darwin'
|
||||||
? 'Alt+Command+I'
|
? 'Alt+Command+I'
|
||||||
: 'Ctrl+Shift+I',
|
: 'Ctrl+Shift+I',
|
||||||
click: toggleDevTools
|
click: () => windows.main.toggleDevTools()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Show WebTorrent Process',
|
label: 'Show WebTorrent Process',
|
||||||
accelerator: process.platform === 'darwin'
|
accelerator: process.platform === 'darwin'
|
||||||
? 'Alt+Command+P'
|
? 'Alt+Command+P'
|
||||||
: 'Ctrl+Shift+P',
|
: 'Ctrl+Shift+P',
|
||||||
click: showWebTorrentWindow
|
click: () => windows.webtorrent.toggleDevTools()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -330,8 +193,8 @@ function getAppMenuTemplate () {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Play/Pause',
|
label: 'Play/Pause',
|
||||||
accelerator: 'CmdOrCtrl+P',
|
accelerator: 'Space',
|
||||||
click: playPause,
|
click: () => windows.main.dispatch('playPause'),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -340,13 +203,13 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
label: 'Increase Volume',
|
label: 'Increase Volume',
|
||||||
accelerator: 'CmdOrCtrl+Up',
|
accelerator: 'CmdOrCtrl+Up',
|
||||||
click: increaseVolume,
|
click: () => windows.main.dispatch('changeVolume', 0.1),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Decrease Volume',
|
label: 'Decrease Volume',
|
||||||
accelerator: 'CmdOrCtrl+Down',
|
accelerator: 'CmdOrCtrl+Down',
|
||||||
click: decreaseVolume,
|
click: () => windows.main.dispatch('changeVolume', -0.1),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -354,14 +217,18 @@ function getAppMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Step Forward',
|
label: 'Step Forward',
|
||||||
accelerator: 'CmdOrCtrl+Alt+Right',
|
accelerator: process.platform === 'darwin'
|
||||||
click: skipForward,
|
? 'CmdOrCtrl+Alt+Right'
|
||||||
|
: 'Alt+Right',
|
||||||
|
click: () => windows.main.dispatch('skip', 1),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Step Backward',
|
label: 'Step Backward',
|
||||||
accelerator: 'CmdOrCtrl+Alt+Left',
|
accelerator: process.platform === 'darwin'
|
||||||
click: skipBack,
|
? 'CmdOrCtrl+Alt+Left'
|
||||||
|
: 'Alt+Left',
|
||||||
|
click: () => windows.main.dispatch('skip', -1),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -370,13 +237,13 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
label: 'Increase Speed',
|
label: 'Increase Speed',
|
||||||
accelerator: 'CmdOrCtrl+=',
|
accelerator: 'CmdOrCtrl+=',
|
||||||
click: increasePlaybackRate,
|
click: () => windows.main.dispatch('changePlaybackRate', 1),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Decrease Speed',
|
label: 'Decrease Speed',
|
||||||
accelerator: 'CmdOrCtrl+-',
|
accelerator: 'CmdOrCtrl+-',
|
||||||
click: decreasePlaybackRate,
|
click: () => windows.main.dispatch('changePlaybackRate', -1),
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -384,7 +251,7 @@ function getAppMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Add Subtitles File...',
|
label: 'Add Subtitles File...',
|
||||||
click: openSubtitles,
|
click: () => windows.main.dispatch('openSubtitles'),
|
||||||
enabled: false
|
enabled: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -395,18 +262,18 @@ function getAppMenuTemplate () {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Learn more about ' + config.APP_NAME,
|
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',
|
label: 'Contribute on GitHub',
|
||||||
click: () => electron.shell.openExternal(config.GITHUB_URL)
|
click: () => shell.openExternal(config.GITHUB_URL)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Report an Issue...',
|
label: 'Report an Issue...',
|
||||||
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES)
|
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -427,7 +294,7 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
label: 'Preferences',
|
label: 'Preferences',
|
||||||
accelerator: 'Cmd+,',
|
accelerator: 'Cmd+,',
|
||||||
click: () => showPreferences()
|
click: () => windows.main.dispatch('preferences')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
@@ -486,12 +353,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') {
|
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||||
// File menu (Windows, Linux)
|
// File menu (Windows, Linux)
|
||||||
template[0].submenu.unshift({
|
template[0].submenu.unshift({
|
||||||
label: 'Create New Torrent from File...',
|
label: 'Create New Torrent from File...',
|
||||||
click: showOpenSeedFile
|
click: () => dialog.openSeedFile()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Help menu (Windows, Linux)
|
// Help menu (Windows, Linux)
|
||||||
@@ -501,12 +369,12 @@ function getAppMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'About ' + config.APP_NAME,
|
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
|
// Add "File > Quit" menu item so Linux distros where the system tray icon is
|
||||||
// will have a way to quit the app.
|
// missing will have a way to quit the app.
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
// File menu (Linux)
|
// File menu (Linux)
|
||||||
template[0].submenu.push({
|
template[0].submenu.push({
|
||||||
@@ -517,23 +385,3 @@ function getAppMenuTemplate () {
|
|||||||
|
|
||||||
return template
|
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}`)
|
||||||
|
}
|
||||||
32
main/shell.js
Normal file
32
main/shell.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
module.exports = {
|
||||||
|
openExternal,
|
||||||
|
openItem,
|
||||||
|
showItemInFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,34 +1,20 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
|
||||||
onPlayerClose,
|
onPlayerClose,
|
||||||
onPlayerOpen
|
onPlayerOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var menu = require('./menu')
|
|
||||||
var windows = require('./windows')
|
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 () {
|
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(
|
electron.globalShortcut.register(
|
||||||
'MediaPlayPause',
|
'MediaPlayPause',
|
||||||
() => windows.main.send('dispatch', 'playPause')
|
() => windows.main.dispatch('playPause')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function onPlayerClose () {
|
||||||
|
// Return the media key to the OS, so other apps can use it.
|
||||||
electron.globalShortcut.unregister('MediaPlayPause')
|
electron.globalShortcut.unregister('MediaPlayPause')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ var app = electron.app
|
|||||||
|
|
||||||
var handlers = require('./handlers')
|
var handlers = require('./handlers')
|
||||||
|
|
||||||
var exeName = path.basename(process.execPath)
|
var EXE_NAME = path.basename(process.execPath)
|
||||||
var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe')
|
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||||
|
|
||||||
function handleEvent (cmd) {
|
function handleEvent (cmd) {
|
||||||
if (cmd === '--squirrel-install') {
|
if (cmd === '--squirrel-install') {
|
||||||
@@ -61,15 +61,17 @@ function handleEvent (cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === '--squirrel-firstrun') {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function spawn (command, args, cb) {
|
||||||
var stdout = ''
|
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) {
|
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) {
|
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) {
|
function updateShortcuts (cb) {
|
||||||
var homeDir = os.homedir()
|
var homeDir = os.homedir()
|
||||||
if (homeDir) {
|
if (homeDir) {
|
||||||
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
||||||
// Check if the desktop shortcut has been previously deleted and and keep it deleted
|
// If the desktop shortcut was deleted by the user, then keep it deleted.
|
||||||
// if it was
|
|
||||||
fs.access(desktopShortcutPath, function (err) {
|
fs.access(desktopShortcutPath, function (err) {
|
||||||
var desktopShortcutExists = !err
|
var desktopShortcutExists = !err
|
||||||
createShortcuts(function () {
|
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) {
|
function removeShortcuts (cb) {
|
||||||
spawnUpdate(['--removeShortcut', exeName], cb)
|
spawnUpdate(['--removeShortcut', EXE_NAME], cb)
|
||||||
}
|
}
|
||||||
|
|||||||
118
main/tray.js
118
main/tray.js
@@ -1,51 +1,62 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
hasTray,
|
||||||
init,
|
init,
|
||||||
hasTray
|
onWindowBlur,
|
||||||
|
onWindowFocus
|
||||||
}
|
}
|
||||||
|
|
||||||
var cp = require('child_process')
|
|
||||||
var path = require('path')
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
var trayIcon
|
var tray
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
// OS X has no tray icon
|
|
||||||
if (process.platform === 'darwin') return
|
|
||||||
|
|
||||||
// On Linux, asynchronously check for libappindicator1
|
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
checkLinuxTraySupport(function (supportsTray) {
|
initLinux()
|
||||||
if (supportsTray) createTrayIcon()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
if (process.platform === 'win32') {
|
||||||
// Windows always supports minimize-to-tray
|
initWin32()
|
||||||
if (process.platform === 'win32') createTrayIcon()
|
}
|
||||||
|
// OS X apps generally do not have menu bar icons
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there a tray icon is active.
|
||||||
|
*/
|
||||||
function hasTray () {
|
function hasTray () {
|
||||||
return !!trayIcon
|
return !!tray
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTrayIcon () {
|
function onWindowBlur () {
|
||||||
trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
|
if (!tray) return
|
||||||
|
|
||||||
// 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
|
|
||||||
updateTrayMenu()
|
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) {
|
function checkLinuxTraySupport (cb) {
|
||||||
|
var cp = require('child_process')
|
||||||
|
|
||||||
// Check that we're on Ubuntu (or another debian system) and that we have
|
// 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
|
// 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.
|
// 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 () {
|
function updateTrayMenu () {
|
||||||
var showHideMenuItem
|
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||||
if (windows.main.isVisible()) {
|
tray.setContextMenu(contextMenu)
|
||||||
showHideMenuItem = { label: 'Hide to tray', click: hideApp }
|
}
|
||||||
} else {
|
|
||||||
showHideMenuItem = { label: 'Show', click: showApp }
|
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 () {
|
function getIconPath () {
|
||||||
windows.main.show()
|
return process.platform === 'win32'
|
||||||
}
|
? config.APP_ICON + '.ico'
|
||||||
|
: config.APP_ICON + '.png'
|
||||||
function hideApp () {
|
|
||||||
windows.main.hide()
|
|
||||||
windows.main.send('dispatch', 'backToList')
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,27 +21,27 @@ function init () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Electron auto-updater does not support Linux yet, so manually check for updates and
|
// The Electron auto-updater does not support Linux yet, so manually check for
|
||||||
// `show the user a modal notification.
|
// updates and show the user a modal notification.
|
||||||
function initLinux () {
|
function initLinux () {
|
||||||
get.concat(AUTO_UPDATE_URL, onResponse)
|
get.concat(AUTO_UPDATE_URL, onResponse)
|
||||||
|
}
|
||||||
|
|
||||||
function onResponse (err, res, data) {
|
function onResponse (err, res, data) {
|
||||||
if (err) return log(`Update error: ${err.message}`)
|
if (err) return log(`Update error: ${err.message}`)
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
// Update available
|
// Update available
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(data)
|
data = JSON.parse(data)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return log(`Update error: Invalid JSON response: ${err.message}`)
|
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}`)
|
|
||||||
}
|
}
|
||||||
|
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')
|
||||||
214
main/windows/main.js
Normal file
214
main/windows/main.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
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', function () {
|
||||||
|
menu.onWindowBlur()
|
||||||
|
tray.onWindowBlur()
|
||||||
|
})
|
||||||
|
|
||||||
|
win.on('focus', function () {
|
||||||
|
menu.onWindowFocus()
|
||||||
|
tray.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()
|
||||||
|
win.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 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
||||||
"version": "0.6.0",
|
"version": "0.7.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@feross.org",
|
"email": "feross@feross.org",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"url": "https://github.com/feross/webtorrent-desktop/issues"
|
"url": "https://github.com/feross/webtorrent-desktop/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"airplay-js": "guerrerocarlos/node-airplay-js",
|
"airplayer": "^2.0.0",
|
||||||
"application-config": "^0.2.1",
|
"application-config": "^0.2.1",
|
||||||
"bitfield": "^1.0.2",
|
"bitfield": "^1.0.2",
|
||||||
"chromecasts": "^1.8.0",
|
"chromecasts": "^1.8.0",
|
||||||
@@ -22,8 +22,7 @@
|
|||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.1.0",
|
"dlnacasts": "^0.1.0",
|
||||||
"drag-drop": "^2.11.0",
|
"drag-drop": "^2.11.0",
|
||||||
"electron-localshortcut": "^0.6.0",
|
"electron-prebuilt": "1.2.1",
|
||||||
"electron-prebuilt": "1.1.1",
|
|
||||||
"fs-extra": "^0.27.0",
|
"fs-extra": "^0.27.0",
|
||||||
"hyperx": "^2.0.2",
|
"hyperx": "^2.0.2",
|
||||||
"iso-639-1": "^1.2.1",
|
"iso-639-1": "^1.2.1",
|
||||||
@@ -31,8 +30,10 @@
|
|||||||
"main-loop": "^3.2.0",
|
"main-loop": "^3.2.0",
|
||||||
"musicmetadata": "^2.0.2",
|
"musicmetadata": "^2.0.2",
|
||||||
"network-address": "^1.1.0",
|
"network-address": "^1.1.0",
|
||||||
|
"parse-torrent": "^5.7.3",
|
||||||
"prettier-bytes": "^1.0.1",
|
"prettier-bytes": "^1.0.1",
|
||||||
"run-parallel": "^1.1.6",
|
"run-parallel": "^1.1.6",
|
||||||
|
"semver": "^5.1.0",
|
||||||
"simple-concat": "^1.0.0",
|
"simple-concat": "^1.0.0",
|
||||||
"simple-get": "^2.0.0",
|
"simple-get": "^2.0.0",
|
||||||
"srt-to-vtt": "^1.1.1",
|
"srt-to-vtt": "^1.1.1",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ module.exports = {
|
|||||||
setRate
|
setRate
|
||||||
}
|
}
|
||||||
|
|
||||||
var airplay = require('airplay-js')
|
var airplayer = require('airplayer')()
|
||||||
var chromecasts = require('chromecasts')()
|
var chromecasts = require('chromecasts')()
|
||||||
var dlnacasts = require('dlnacasts')()
|
var dlnacasts = require('dlnacasts')()
|
||||||
|
|
||||||
@@ -41,10 +41,9 @@ function init (appState, callback) {
|
|||||||
state.devices.dlna = dlnaPlayer(player)
|
state.devices.dlna = dlnaPlayer(player)
|
||||||
})
|
})
|
||||||
|
|
||||||
var browser = airplay.createBrowser()
|
airplayer.on('update', function (player) {
|
||||||
browser.on('deviceOn', function (player) {
|
|
||||||
state.devices.airplay = airplayPlayer(player)
|
state.devices.airplay = airplayPlayer(player)
|
||||||
}).start()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// chromecast player implementation
|
// chromecast player implementation
|
||||||
@@ -129,13 +128,31 @@ function chromecastPlayer (player) {
|
|||||||
|
|
||||||
// airplay player implementation
|
// airplay player implementation
|
||||||
function airplayPlayer (player) {
|
function airplayPlayer (player) {
|
||||||
|
function addEvents () {
|
||||||
|
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 open () {
|
function open () {
|
||||||
player.play(state.server.networkURL, 0, function (res) {
|
player.play(state.server.networkURL, function (err, res) {
|
||||||
if (res.statusCode !== 200) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
message: 'Could not connect to AirPlay.'
|
message: 'Could not connect to AirPlay. ' + err.message
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
state.playing.location = 'airplay'
|
state.playing.location = 'airplay'
|
||||||
@@ -145,11 +162,11 @@ function airplayPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
player.rate(1, callback)
|
player.resume(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
player.rate(0, callback)
|
player.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
@@ -157,13 +174,18 @@ function airplayPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
player.status(function (status) {
|
player.playbackInfo(function (err, res, status) {
|
||||||
state.playing.isPaused = status.rate === 0
|
if (err) {
|
||||||
state.playing.currentTime = status.position
|
state.playing.location = 'local'
|
||||||
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume
|
state.errors.push({
|
||||||
// According to docs is in [-30 - 0] (db) range
|
time: new Date().getTime(),
|
||||||
// should be converted to [0 - 1] using (val / 30 + 1)
|
message: 'Could not connect to AirPlay. ' + err.message
|
||||||
update()
|
})
|
||||||
|
} else {
|
||||||
|
state.playing.isPaused = status.rate === 0
|
||||||
|
state.playing.currentTime = status.position
|
||||||
|
update()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,12 +194,13 @@ function airplayPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, 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
|
state.playing.volume = volume
|
||||||
volume = (volume - 1) * 30
|
|
||||||
player.volume(volume, callback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addEvents()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
player: player,
|
player: player,
|
||||||
open: open,
|
open: open,
|
||||||
|
|||||||
@@ -1,36 +1,39 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
setDispatch,
|
|
||||||
dispatch,
|
dispatch,
|
||||||
dispatcher
|
dispatcher,
|
||||||
|
setDispatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoize most of our event handlers, which are functions in the form
|
var dispatchers = {}
|
||||||
// () => dispatch(<args>)
|
var _dispatch = function () {}
|
||||||
// ... this prevents virtual-dom from updating every listener on every update()
|
|
||||||
var _dispatchers = {}
|
|
||||||
var _dispatch = () => {}
|
|
||||||
|
|
||||||
function setDispatch (dispatch) {
|
function setDispatch (dispatch) {
|
||||||
_dispatch = dispatch
|
_dispatch = dispatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a _memoized event handler that calls dispatch()
|
function dispatch (...args) {
|
||||||
// All args must be JSON-able
|
_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) {
|
function dispatcher (...args) {
|
||||||
var json = JSON.stringify(args)
|
var str = JSON.stringify(args)
|
||||||
var handler = _dispatchers[json]
|
var handler = dispatchers[str]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
handler = _dispatchers[json] = (e) => {
|
handler = dispatchers[str] = function (e) {
|
||||||
// Don't click on whatever is below the button
|
// Do not propagate click to elements below the button
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
// Don't regisiter clicks on disabled buttons
|
|
||||||
if (e.currentTarget.classList.contains('disabled')) return
|
if (e.currentTarget.classList.contains('disabled')) {
|
||||||
_dispatch.apply(null, args)
|
// Ignore clicks on disabled elements
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(...args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return handler
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
215
renderer/lib/state.js
Normal file
215
renderer/lib/state.js
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
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 */
|
||||||
|
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,
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -10,14 +10,14 @@ var config = require('../../config')
|
|||||||
// Returns an absolute path to the torrent file, or null if unavailable
|
// Returns an absolute path to the torrent file, or null if unavailable
|
||||||
function getTorrentPath (torrentSummary) {
|
function getTorrentPath (torrentSummary) {
|
||||||
if (!torrentSummary || !torrentSummary.torrentFileName) return null
|
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
|
// Expects a torrentSummary
|
||||||
// Returns an absolute path to the poster image, or null if unavailable
|
// Returns an absolute path to the poster image, or null if unavailable
|
||||||
function getPosterPath (torrentSummary) {
|
function getPosterPath (torrentSummary) {
|
||||||
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
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):
|
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
||||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||||
return posterPath.replace(/\\/g, '/')
|
return posterPath.replace(/\\/g, '/')
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ table {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
background: rgb(40, 40, 40);
|
background: rgb(40, 40, 40);
|
||||||
animation: fadein 1s;
|
animation: fadein 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app:not(.is-focused) {
|
.app:not(.is-focused) {
|
||||||
@@ -280,36 +280,36 @@ table {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-page {
|
.create-torrent {
|
||||||
padding: 10px 25px;
|
padding: 10px 25px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-page .torrent-attribute {
|
.create-torrent .torrent-attribute {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-page .torrent-attribute>* {
|
.create-torrent .torrent-attribute>* {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-page .torrent-attribute label {
|
.create-torrent .torrent-attribute label {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-page .torrent-attribute>div {
|
.create-torrent .torrent-attribute>div {
|
||||||
width: calc(100% - 90px);
|
width: calc(100% - 90px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-page .torrent-attribute div {
|
.create-torrent .torrent-attribute div {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-page .torrent-attribute textarea {
|
.create-torrent .torrent-attribute textarea {
|
||||||
width: calc(100% - 80px);
|
width: calc(100% - 80px);
|
||||||
height: 80px;
|
height: 80px;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
@@ -321,11 +321,11 @@ table {
|
|||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-page textarea.torrent-trackers {
|
.create-torrent textarea.torrent-trackers {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-page input.torrent-is-private {
|
.create-torrent input.torrent-is-private {
|
||||||
width: initial;
|
width: initial;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -605,6 +605,10 @@ body.drag .app::after {
|
|||||||
padding: 8em 0 20px 0;
|
padding: 8em 0 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent-details .warning {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.torrent-details table {
|
.torrent-details table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -711,10 +715,7 @@ body.drag .app::after {
|
|||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
|
|
||||||
/*
|
/* Make all icons have uniform spacing */
|
||||||
* Fix for overflowing captions icon
|
|
||||||
* https://github.com/feross/webtorrent-desktop/issues/467
|
|
||||||
*/
|
|
||||||
max-width: 23px;
|
max-width: 23px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -750,7 +751,7 @@ body.drag .app::after {
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls .icon.closed-captions {
|
.player .controls .icon.closed-caption {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
@@ -818,7 +819,7 @@ body.drag .app::after {
|
|||||||
transition-timing-function: ease-out;
|
transition-timing-function: ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls .closed-captions.active,
|
.player .controls .closed-caption.active,
|
||||||
.player .controls .device.active {
|
.player .controls .device.active {
|
||||||
color: #9af;
|
color: #9af;
|
||||||
}
|
}
|
||||||
@@ -847,6 +848,28 @@ body.drag .app::after {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player .controls .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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .subtitles-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.
|
* Set the cue text position so it appears above the player controls.
|
||||||
*/
|
*/
|
||||||
@@ -889,29 +912,6 @@ video::-webkit-media-text-track-container {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Subtitles list
|
|
||||||
*/
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitles-list i {
|
|
||||||
font-size: 11px; /* make the cast icons less huge */
|
|
||||||
margin-right: 4px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Preferences page, based on Atom settings style
|
* Preferences page, based on Atom settings style
|
||||||
*/
|
*/
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="index.css" charset="utf-8">
|
<link rel="stylesheet" href="main.css" charset="utf-8">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script async src="index.js"></script>
|
<script async src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,18 +3,8 @@ console.time('init')
|
|||||||
var crashReporter = require('../crash-reporter')
|
var crashReporter = require('../crash-reporter')
|
||||||
crashReporter.init()
|
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 dragDrop = require('drag-drop')
|
var dragDrop = require('drag-drop')
|
||||||
|
var electron = require('electron')
|
||||||
var fs = require('fs-extra')
|
var fs = require('fs-extra')
|
||||||
var mainLoop = require('main-loop')
|
var mainLoop = require('main-loop')
|
||||||
var parallel = require('run-parallel')
|
var parallel = require('run-parallel')
|
||||||
@@ -28,39 +18,29 @@ var App = require('./views/app')
|
|||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var errors = require('./lib/errors')
|
var errors = require('./lib/errors')
|
||||||
var sound = require('./lib/sound')
|
var sound = require('./lib/sound')
|
||||||
var State = require('./state')
|
var State = require('./lib/state')
|
||||||
var TorrentPlayer = require('./lib/torrent-player')
|
var TorrentPlayer = require('./lib/torrent-player')
|
||||||
var TorrentSummary = require('./lib/torrent-summary')
|
var TorrentSummary = require('./lib/torrent-summary')
|
||||||
|
|
||||||
var {setDispatch} = require('./lib/dispatcher')
|
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
|
// This dependency is the slowest-loading, so we lazy load it
|
||||||
var Cast = null
|
var Cast = null
|
||||||
|
|
||||||
var vdomLoop
|
init()
|
||||||
|
|
||||||
var state = State.getInitialState()
|
function init () {
|
||||||
state.location.go({ url: 'home' }) // Add first page to location history
|
// 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)
|
||||||
|
|
||||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
setDispatch(dispatch)
|
||||||
// All other state is ephemeral. First we load state.saved then initialize the app.
|
|
||||||
loadState(init)
|
|
||||||
|
|
||||||
function loadState (cb) {
|
|
||||||
appConfig.read(function (err, data) {
|
|
||||||
if (err) console.error(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()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,9 +48,12 @@ function loadState (cb) {
|
|||||||
* Connects to the torrent networks, sets up the UI and OS integrations like
|
* Connects to the torrent networks, sets up the UI and OS integrations like
|
||||||
* the dock icon and drag+drop.
|
* the dock icon and drag+drop.
|
||||||
*/
|
*/
|
||||||
function init () {
|
function onState (err, _state) {
|
||||||
// Clean up the freshly-loaded config file, which may be from an older version
|
if (err) return onError(err)
|
||||||
cleanUpConfig()
|
state = _state
|
||||||
|
|
||||||
|
// Add first page to location history
|
||||||
|
state.location.go({ url: 'home' })
|
||||||
|
|
||||||
// Restart everything we were torrenting last time the app ran
|
// Restart everything we were torrenting last time the app ran
|
||||||
resumeTorrents()
|
resumeTorrents()
|
||||||
@@ -89,6 +72,9 @@ function init () {
|
|||||||
})
|
})
|
||||||
document.body.appendChild(vdomLoop.target)
|
document.body.appendChild(vdomLoop.target)
|
||||||
|
|
||||||
|
// Listen for messages from the main process
|
||||||
|
setupIpc()
|
||||||
|
|
||||||
// Calling update() updates the UI given the current state
|
// Calling update() updates the UI given the current state
|
||||||
// Do this at least once a second to give every file in every torrentSummary
|
// Do this at least once a second to give every file in every torrentSummary
|
||||||
// a progress bar and to keep the cursor in sync when playing a video
|
// a progress bar and to keep the cursor in sync when playing a video
|
||||||
@@ -105,9 +91,9 @@ function init () {
|
|||||||
window.addEventListener('focus', onFocus)
|
window.addEventListener('focus', onFocus)
|
||||||
window.addEventListener('blur', onBlur)
|
window.addEventListener('blur', onBlur)
|
||||||
|
|
||||||
// Done! Ideally we want to get here <100ms after the user clicks the app
|
|
||||||
sound.play('STARTUP')
|
sound.play('STARTUP')
|
||||||
|
|
||||||
|
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,60 +102,6 @@ function delayedInit () {
|
|||||||
sound.preload()
|
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lazily loads Chromecast and Airplay support
|
// Lazily loads Chromecast and Airplay support
|
||||||
function lazyLoadCast () {
|
function lazyLoadCast () {
|
||||||
if (!Cast) {
|
if (!Cast) {
|
||||||
@@ -192,7 +124,7 @@ function render (state) {
|
|||||||
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
|
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
|
||||||
function update () {
|
function update () {
|
||||||
showOrHidePlayerControls()
|
showOrHidePlayerControls()
|
||||||
if (vdomLoop) vdomLoop.update(state)
|
vdomLoop.update(state)
|
||||||
updateElectron()
|
updateElectron()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,17 +156,24 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'addTorrent') {
|
if (action === 'addTorrent') {
|
||||||
addTorrent(args[0] /* torrent */)
|
addTorrent(args[0] /* torrent */)
|
||||||
}
|
}
|
||||||
if (action === 'showOpenTorrentFile') {
|
if (action === 'openTorrentFile') {
|
||||||
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
|
ipcRenderer.send('openTorrentFile') /* open torrent file */
|
||||||
|
}
|
||||||
|
if (action === 'openFiles') {
|
||||||
|
ipcRenderer.send('openFiles') /* add files with dialog */
|
||||||
}
|
}
|
||||||
if (action === 'showCreateTorrent') {
|
if (action === 'showCreateTorrent') {
|
||||||
showCreateTorrent(args[0] /* fileOrFolder */)
|
showCreateTorrent(args[0] /* paths */)
|
||||||
|
}
|
||||||
|
if (action === 'openTorrentAddress') {
|
||||||
|
state.modal = { id: 'open-torrent-address-modal' }
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
if (action === 'createTorrent') {
|
if (action === 'createTorrent') {
|
||||||
createTorrent(args[0] /* options */)
|
createTorrent(args[0] /* options */)
|
||||||
}
|
}
|
||||||
if (action === 'openFile') {
|
if (action === 'openItem') {
|
||||||
openFile(args[0] /* infoHash */, args[1] /* index */)
|
openItem(args[0] /* infoHash */, args[1] /* index */)
|
||||||
}
|
}
|
||||||
if (action === 'toggleTorrent') {
|
if (action === 'toggleTorrent') {
|
||||||
toggleTorrent(args[0] /* infoHash */)
|
toggleTorrent(args[0] /* infoHash */)
|
||||||
@@ -375,7 +314,10 @@ function dispatch (action, ...args) {
|
|||||||
saveStateThrottled()
|
saveStateThrottled()
|
||||||
}
|
}
|
||||||
if (action === 'saveState') {
|
if (action === 'saveState') {
|
||||||
saveState()
|
State.save(state)
|
||||||
|
}
|
||||||
|
if (action === 'setTitle') {
|
||||||
|
state.window.title = args[0] /* title */
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the virtual-dom, unless it's just a mouse move event
|
// Update the virtual-dom, unless it's just a mouse move event
|
||||||
@@ -501,18 +443,11 @@ function isCasting () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupIpc () {
|
function setupIpc () {
|
||||||
ipcRenderer.send('ipcReady')
|
|
||||||
|
|
||||||
ipcRenderer.on('log', (e, ...args) => console.log(...args))
|
ipcRenderer.on('log', (e, ...args) => console.log(...args))
|
||||||
ipcRenderer.on('error', (e, ...args) => console.error(...args))
|
ipcRenderer.on('error', (e, ...args) => console.error(...args))
|
||||||
|
|
||||||
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...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) {
|
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
|
||||||
state.window.isFullScreen = isFullScreen
|
state.window.isFullScreen = isFullScreen
|
||||||
if (!isFullScreen) {
|
if (!isFullScreen) {
|
||||||
@@ -534,13 +469,15 @@ function setupIpc () {
|
|||||||
ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args))
|
ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args))
|
||||||
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
|
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
|
||||||
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
||||||
|
|
||||||
|
ipcRenderer.send('ipcReady')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts all torrents that aren't paused on program startup
|
// Starts all torrents that aren't paused on program startup
|
||||||
function resumeTorrents () {
|
function resumeTorrents () {
|
||||||
state.saved.torrents
|
state.saved.torrents
|
||||||
.filter((x) => x.status !== 'paused')
|
.filter((torrentSummary) => torrentSummary.status !== 'paused')
|
||||||
.forEach((x) => startTorrentingSummary(x))
|
.forEach((torrentSummary) => startTorrentingSummary(torrentSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates a single property in the UNSAVED prefs
|
// Updates a single property in the UNSAVED prefs
|
||||||
@@ -561,7 +498,8 @@ function updatePreferences (property, value) {
|
|||||||
// All unsaved prefs take effect atomically, and are saved to config.json
|
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||||
function savePreferences () {
|
function savePreferences () {
|
||||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||||
saveState()
|
State.save(state)
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't write state.saved to file more than once a second
|
// Don't write state.saved to file more than once a second
|
||||||
@@ -569,44 +507,13 @@ function saveStateThrottled () {
|
|||||||
if (state.saveStateTimeout) return
|
if (state.saveStateTimeout) return
|
||||||
state.saveStateTimeout = setTimeout(function () {
|
state.saveStateTimeout = setTimeout(function () {
|
||||||
delete state.saveStateTimeout
|
delete state.saveStateTimeout
|
||||||
saveState()
|
State.save(state)
|
||||||
|
update()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write state.saved to the JSON state file
|
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
|
||||||
function saveState () {
|
// via any method (drag-drop, drag to app icon, command line)
|
||||||
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') {
|
|
||||||
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
|
|
||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
if (!Array.isArray(files)) files = [ files ]
|
if (!Array.isArray(files)) files = [ files ]
|
||||||
|
|
||||||
@@ -667,6 +574,7 @@ function addTorrent (torrentId) {
|
|||||||
torrentId = torrentId.path
|
torrentId = torrentId.path
|
||||||
}
|
}
|
||||||
// Allow a instant.io link to be pasted
|
// 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)) {
|
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||||
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||||
}
|
}
|
||||||
@@ -674,12 +582,13 @@ function addTorrent (torrentId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addSubtitles (files, autoSelect) {
|
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 (state.playing.type !== 'video') return
|
||||||
|
if (files.length === 0) return
|
||||||
|
|
||||||
// Read the files concurrently, then add all resulting subtitle tracks
|
// Read the files concurrently, then add all resulting subtitle tracks
|
||||||
var jobs = files.map((file) => (cb) => loadSubtitle(file, cb))
|
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||||
parallel(jobs, function (err, tracks) {
|
parallel(tasks, function (err, tracks) {
|
||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
|
|
||||||
for (var i = 0; i < tracks.length; i++) {
|
for (var i = 0; i < tracks.length; i++) {
|
||||||
@@ -803,7 +712,9 @@ function startTorrentingSummary (torrentSummary) {
|
|||||||
|
|
||||||
// Shows the Create Torrent page with options to seed a given file or folder
|
// Shows the Create Torrent page with options to seed a given file or folder
|
||||||
function showCreateTorrent (files) {
|
function showCreateTorrent (files) {
|
||||||
if (Array.isArray(files)) {
|
// 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({
|
state.location.go({
|
||||||
url: 'create-torrent',
|
url: 'create-torrent',
|
||||||
files: files
|
files: files
|
||||||
@@ -811,13 +722,29 @@ function showCreateTorrent (files) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileOrFolder = files
|
// ... or it will be an array of mixed file and folder paths. We have to walk
|
||||||
findFilesRecursive(fileOrFolder, showCreateTorrent)
|
// through all the folders and find the files
|
||||||
|
findFilesRecursive(files, showCreateTorrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively finds {name, path, size} for all files in a folder
|
// Recursively finds {name, path, size} for all files in a folder
|
||||||
// Calls `cb` on success, calls `onError` on failure
|
// 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) {
|
||||||
|
cb(ret)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileOrFolder = paths[0]
|
||||||
fs.stat(fileOrFolder, function (err, stat) {
|
fs.stat(fileOrFolder, function (err, stat) {
|
||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
|
|
||||||
@@ -835,16 +762,8 @@ function findFilesRecursive (fileOrFolder, cb) {
|
|||||||
var folderPath = fileOrFolder
|
var folderPath = fileOrFolder
|
||||||
fs.readdir(folderPath, function (err, fileNames) {
|
fs.readdir(folderPath, function (err, fileNames) {
|
||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
var numComplete = 0
|
var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
||||||
var ret = []
|
findFilesRecursive(paths, cb)
|
||||||
fileNames.forEach(function (fileName) {
|
|
||||||
findFilesRecursive(path.join(folderPath, fileName), function (fileObjs) {
|
|
||||||
ret = ret.concat(fileObjs)
|
|
||||||
if (++numComplete === fileNames.length) {
|
|
||||||
cb(ret)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -864,6 +783,12 @@ function torrentInfoHash (torrentKey, infoHash) {
|
|||||||
torrentSummary ? 'existing' : 'new', torrentKey)
|
torrentSummary ? 'existing' : 'new', torrentKey)
|
||||||
|
|
||||||
if (!torrentSummary) {
|
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 = {
|
torrentSummary = {
|
||||||
torrentKey: torrentKey,
|
torrentKey: torrentKey,
|
||||||
status: 'new'
|
status: 'new'
|
||||||
@@ -964,7 +889,10 @@ function torrentProgress (progressInfo) {
|
|||||||
torrentSummary.progress = p
|
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()
|
update()
|
||||||
}
|
}
|
||||||
@@ -1121,6 +1049,9 @@ function closePlayer (cb) {
|
|||||||
ipcRenderer.send('vlcQuit')
|
ipcRenderer.send('vlcQuit')
|
||||||
}
|
}
|
||||||
state.window.title = config.APP_WINDOW_TITLE
|
state.window.title = config.APP_WINDOW_TITLE
|
||||||
|
// Lets save volume for later
|
||||||
|
state.previousVolume = state.playing.volume
|
||||||
|
|
||||||
state.playing = State.getDefaultPlayState()
|
state.playing = State.getDefaultPlayState()
|
||||||
state.server = null
|
state.server = null
|
||||||
|
|
||||||
@@ -1137,7 +1068,7 @@ function closePlayer (cb) {
|
|||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFile (infoHash, index) {
|
function openItem (infoHash, index) {
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
var filePath = path.join(
|
var filePath = path.join(
|
||||||
torrentSummary.path,
|
torrentSummary.path,
|
||||||
@@ -1302,7 +1233,7 @@ function showDoneNotification (torrent) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
notif.onclick = function () {
|
notif.onclick = function () {
|
||||||
ipcRenderer.send('focusWindow', 'main')
|
ipcRenderer.send('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
sound.play('DONE')
|
sound.play('DONE')
|
||||||
@@ -1,286 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
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 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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
prefs: {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlayingFileSummary () {
|
|
||||||
var torrentSummary = this.getPlayingTorrentSummary()
|
|
||||||
if (!torrentSummary) return null
|
|
||||||
return torrentSummary.files[this.playing.fileIndex]
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
module.exports = App
|
module.exports = App
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
var hx = require('../lib/hx')
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
|
|
||||||
var Header = require('./header')
|
var Header = require('./header')
|
||||||
|
|
||||||
var Views = {
|
var Views = {
|
||||||
'home': require('./torrent-list'),
|
'home': require('./home'),
|
||||||
'player': require('./player'),
|
'player': require('./player'),
|
||||||
'create-torrent': require('./create-torrent-page'),
|
'create-torrent': require('./create-torrent'),
|
||||||
'preferences': require('./preferences')
|
'preferences': require('./preferences')
|
||||||
}
|
}
|
||||||
|
|
||||||
var Modals = {
|
var Modals = {
|
||||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||||
'update-available-modal': require('./update-available-modal'),
|
'update-available-modal': require('./update-available-modal'),
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
module.exports = CreateTorrentPage
|
module.exports = CreateTorrentPage
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
|
|
||||||
var createTorrent = require('create-torrent')
|
var createTorrent = require('create-torrent')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var prettyBytes = require('prettier-bytes')
|
var prettyBytes = require('prettier-bytes')
|
||||||
|
|
||||||
var {dispatch} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
var hx = require('../lib/hx')
|
||||||
|
|
||||||
function CreateTorrentPage (state) {
|
function CreateTorrentPage (state) {
|
||||||
var info = state.location.current()
|
var info = state.location.current()
|
||||||
@@ -17,17 +14,14 @@ function CreateTorrentPage (state) {
|
|||||||
var files = info.files
|
var files = info.files
|
||||||
.filter((f) => !f.name.startsWith('.'))
|
.filter((f) => !f.name.startsWith('.'))
|
||||||
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
.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
|
// First, extract the base folder that the files are all in
|
||||||
var pathPrefix = info.folderPath
|
var pathPrefix = info.folderPath
|
||||||
if (!pathPrefix) {
|
if (!pathPrefix) {
|
||||||
if (files.length > 0) {
|
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||||
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
pathPrefix = path.dirname(pathPrefix)
|
||||||
pathPrefix = path.dirname(pathPrefix)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pathPrefix = files[0]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +56,7 @@ function CreateTorrentPage (state) {
|
|||||||
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
|
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
|
||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='create-torrent-page'>
|
<div class='create-torrent'>
|
||||||
<h2>Create torrent ${defaultName}</h2>
|
<h2>Create torrent ${defaultName}</h2>
|
||||||
<p class="torrent-info">
|
<p class="torrent-info">
|
||||||
${torrentInfo}
|
${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
|
// Finds the longest common prefix
|
||||||
function findCommonPrefix (a, b) {
|
function findCommonPrefix (a, b) {
|
||||||
for (var i = 0; i < a.length && i < b.length; i++) {
|
for (var i = 0; i < a.length && i < b.length; i++) {
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
module.exports = Header
|
module.exports = Header
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
|
|
||||||
var {dispatcher} = require('../lib/dispatcher')
|
var {dispatcher} = require('../lib/dispatcher')
|
||||||
|
var hx = require('../lib/hx')
|
||||||
|
|
||||||
function Header (state) {
|
function Header (state) {
|
||||||
return hx`
|
return hx`
|
||||||
@@ -42,7 +39,7 @@ function Header (state) {
|
|||||||
<i
|
<i
|
||||||
class='icon add'
|
class='icon add'
|
||||||
title='Add torrent'
|
title='Add torrent'
|
||||||
onclick=${dispatcher('showOpenTorrentFile')}>
|
onclick=${dispatcher('openFiles')}>
|
||||||
add
|
add
|
||||||
</i>
|
</i>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
module.exports = TorrentList
|
module.exports = TorrentList
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
var prettyBytes = require('prettier-bytes')
|
var prettyBytes = require('prettier-bytes')
|
||||||
|
|
||||||
|
var hx = require('../lib/hx')
|
||||||
var TorrentSummary = require('../lib/torrent-summary')
|
var TorrentSummary = require('../lib/torrent-summary')
|
||||||
var TorrentPlayer = require('../lib/torrent-player')
|
var TorrentPlayer = require('../lib/torrent-player')
|
||||||
var {dispatcher} = require('../lib/dispatcher')
|
var {dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
function TorrentList (state) {
|
function TorrentList (state) {
|
||||||
var torrentRows = state.saved.torrents.map(
|
var torrentRows = state.saved.torrents.map(
|
||||||
(torrentSummary) => renderTorrent(torrentSummary))
|
(torrentSummary) => renderTorrent(torrentSummary)
|
||||||
|
)
|
||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='torrent-list'>
|
<div class='torrent-list'>
|
||||||
${torrentRows}
|
${torrentRows}
|
||||||
@@ -20,11 +20,7 @@ function TorrentList (state) {
|
|||||||
</div>
|
</div>
|
||||||
</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) {
|
function renderTorrent (torrentSummary) {
|
||||||
// Get ephemeral data (like progress %) directly from the WebTorrent handle
|
|
||||||
var infoHash = torrentSummary.infoHash
|
var infoHash = torrentSummary.infoHash
|
||||||
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||||
|
|
||||||
@@ -195,12 +191,13 @@ function TorrentList (state) {
|
|||||||
} else {
|
} else {
|
||||||
// We do know the files. List them and show download stats for each one
|
// We do know the files. List them and show download stats for each one
|
||||||
var fileRows = torrentSummary.files
|
var fileRows = torrentSummary.files
|
||||||
|
.map((file, index) => ({ file, index }))
|
||||||
.sort(function (a, b) {
|
.sort(function (a, b) {
|
||||||
if (a.name < b.name) return -1
|
if (a.file.name < b.file.name) return -1
|
||||||
if (b.name < a.name) return 1
|
if (b.file.name < a.file.name) return 1
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
.map((file, index) => renderFileRow(torrentSummary, file, index))
|
.map((object) => renderFileRow(torrentSummary, object.file, object.index))
|
||||||
|
|
||||||
filesElement = hx`
|
filesElement = hx`
|
||||||
<div class='files'>
|
<div class='files'>
|
||||||
@@ -221,7 +218,8 @@ function TorrentList (state) {
|
|||||||
// Show a single torrentSummary file in the details view for a single torrent
|
// Show a single torrentSummary file in the details view for a single torrent
|
||||||
function renderFileRow (torrentSummary, file, index) {
|
function renderFileRow (torrentSummary, file, index) {
|
||||||
// First, find out how much of the file we've downloaded
|
// First, find out how much of the file we've downloaded
|
||||||
var 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 isDone = false // Are we finished torrenting it?
|
||||||
var progress = ''
|
var progress = ''
|
||||||
if (torrentSummary.progress && torrentSummary.progress.files) {
|
if (torrentSummary.progress && torrentSummary.progress.files) {
|
||||||
@@ -247,7 +245,7 @@ function TorrentList (state) {
|
|||||||
handleClick = dispatcher('play', infoHash, index)
|
handleClick = dispatcher('play', infoHash, index)
|
||||||
} else {
|
} else {
|
||||||
icon = 'description' /* file icon, opens in OS default app */
|
icon = 'description' /* file icon, opens in OS default app */
|
||||||
handleClick = dispatcher('openFile', infoHash, index)
|
handleClick = dispatcher('openItem', infoHash, index)
|
||||||
}
|
}
|
||||||
var rowClass = ''
|
var rowClass = ''
|
||||||
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
module.exports = OpenTorrentAddressModal
|
module.exports = OpenTorrentAddressModal
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
|
|
||||||
var {dispatch} = require('../lib/dispatcher')
|
var {dispatch} = require('../lib/dispatcher')
|
||||||
|
var hx = require('../lib/hx')
|
||||||
|
|
||||||
function OpenTorrentAddressModal (state) {
|
function OpenTorrentAddressModal (state) {
|
||||||
return hx`
|
return hx`
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
module.exports = Player
|
module.exports = Player
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
|
|
||||||
var Bitfield = require('bitfield')
|
var Bitfield = require('bitfield')
|
||||||
var prettyBytes = require('prettier-bytes')
|
var prettyBytes = require('prettier-bytes')
|
||||||
var zeroFill = require('zero-fill')
|
var zeroFill = require('zero-fill')
|
||||||
|
|
||||||
|
var hx = require('../lib/hx')
|
||||||
var TorrentSummary = require('../lib/torrent-summary')
|
var TorrentSummary = require('../lib/torrent-summary')
|
||||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
@@ -37,7 +34,8 @@ function renderMedia (state) {
|
|||||||
|
|
||||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||||
// Instead, grab the DOM node and play/pause it if necessary
|
// Instead, grab the DOM node and play/pause it if necessary
|
||||||
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
|
// Get the <video> or <audio> tag
|
||||||
|
var mediaElement = document.querySelector(state.playing.type)
|
||||||
if (mediaElement !== null) {
|
if (mediaElement !== null) {
|
||||||
if (state.playing.isPaused && !mediaElement.paused) {
|
if (state.playing.isPaused && !mediaElement.paused) {
|
||||||
mediaElement.pause()
|
mediaElement.pause()
|
||||||
@@ -52,6 +50,12 @@ function renderMedia (state) {
|
|||||||
if (state.playing.playbackRate !== mediaElement.playbackRate) {
|
if (state.playing.playbackRate !== mediaElement.playbackRate) {
|
||||||
mediaElement.playbackRate = state.playing.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
|
// Set volume
|
||||||
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
||||||
mediaElement.volume = state.playing.setVolume
|
mediaElement.volume = state.playing.setVolume
|
||||||
@@ -59,9 +63,10 @@ function renderMedia (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Switch to the newly added subtitle track, if available
|
// 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++) {
|
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'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save video position
|
// Save video position
|
||||||
@@ -114,7 +119,7 @@ function renderMedia (state) {
|
|||||||
</div>
|
</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) {
|
function onLoadedMetadata (e) {
|
||||||
if (state.playing.type !== 'video') return
|
if (state.playing.type !== 'video') return
|
||||||
var video = e.target
|
var video = e.target
|
||||||
@@ -132,7 +137,8 @@ function renderMedia (state) {
|
|||||||
|
|
||||||
function onCanPlay (e) {
|
function onCanPlay (e) {
|
||||||
var elem = e.target
|
var elem = e.target
|
||||||
if (state.playing.type === 'video' && elem.webkitVideoDecodedByteCount === 0) {
|
if (state.playing.type === 'video' &&
|
||||||
|
elem.webkitVideoDecodedByteCount === 0) {
|
||||||
dispatch('mediaError', 'Video codec unsupported')
|
dispatch('mediaError', 'Video codec unsupported')
|
||||||
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
||||||
dispatch('mediaError', 'Audio codec unsupported')
|
dispatch('mediaError', 'Audio codec unsupported')
|
||||||
@@ -157,7 +163,8 @@ function renderOverlay (state) {
|
|||||||
} else if (elems.length !== 0) {
|
} else if (elems.length !== 0) {
|
||||||
style = { backgroundImage: cssBackgroundImageDarkGradient() }
|
style = { backgroundImage: cssBackgroundImageDarkGradient() }
|
||||||
} else {
|
} 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`
|
return hx`
|
||||||
@@ -187,15 +194,37 @@ function renderAudioMetadata (state) {
|
|||||||
track = info.track.no + ' of ' + info.track.of
|
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 = []
|
var elems = []
|
||||||
if (artist) elems.push(hx`<div class='audio-artist'><label>Artist</label>${artist}</div>`)
|
if (artist) {
|
||||||
if (album) elems.push(hx`<div class='audio-album'><label>Album</label>${album}</div>`)
|
elems.push(hx`
|
||||||
if (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`)
|
<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>`
|
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>`
|
return hx`<div class='audio-metadata'>${elems}</div>`
|
||||||
}
|
}
|
||||||
@@ -278,18 +307,19 @@ function renderSubtitlesOptions (state) {
|
|||||||
var isSelected = state.playing.subtitles.selectedIndex === ix
|
var isSelected = state.playing.subtitles.selectedIndex === ix
|
||||||
return hx`
|
return hx`
|
||||||
<li onclick=${dispatcher('selectSubtitle', ix)}>
|
<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}
|
${track.label}
|
||||||
</li>
|
</li>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||||
|
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||||
return hx`
|
return hx`
|
||||||
<ul.subtitles-list>
|
<ul.subtitles-list>
|
||||||
${items}
|
${items}
|
||||||
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
||||||
<i.icon>${noneSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
<i.icon>${noneClass}</i>
|
||||||
None
|
None
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -339,10 +369,10 @@ function renderPlayerControls (state) {
|
|||||||
if (state.playing.type === 'video') {
|
if (state.playing.type === 'video') {
|
||||||
// show closed captions icon
|
// show closed captions icon
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.closed-captions.float-right
|
<i.icon.closed-caption.float-right
|
||||||
class=${captionsClass}
|
class=${captionsClass}
|
||||||
onclick=${handleSubtitles}>
|
onclick=${handleSubtitles}>
|
||||||
closed_captions
|
closed_caption
|
||||||
</i>
|
</i>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
@@ -351,7 +381,9 @@ function renderPlayerControls (state) {
|
|||||||
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
||||||
var isOnAirplay = state.playing.location.startsWith('airplay')
|
var isOnAirplay = state.playing.location.startsWith('airplay')
|
||||||
var isOnDlna = state.playing.location.startsWith('dlna')
|
var isOnDlna = state.playing.location.startsWith('dlna')
|
||||||
var chromecastClass, chromecastHandler, airplayClass, airplayHandler, dlnaClass, dlnaHandler
|
var chromecastClass, chromecastHandler
|
||||||
|
var airplayClass, airplayHandler
|
||||||
|
var dlnaClass, dlnaHandler
|
||||||
if (isOnChromecast) {
|
if (isOnChromecast) {
|
||||||
chromecastClass = 'active'
|
chromecastClass = 'active'
|
||||||
dlnaClass = 'disabled'
|
dlnaClass = 'disabled'
|
||||||
@@ -413,10 +445,15 @@ function renderPlayerControls (state) {
|
|||||||
|
|
||||||
// render volume
|
// render volume
|
||||||
var volume = state.playing.volume
|
var volume = state.playing.volume
|
||||||
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
|
var volumeIcon = 'volume_' + (
|
||||||
var volumeStyle = { background: '-webkit-gradient(linear, left top, right top, ' +
|
volume === 0 ? 'off'
|
||||||
'color-stop(' + (volume * 100) + '%, #eee), ' +
|
: volume < 0.3 ? 'mute'
|
||||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
: 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`
|
elements.push(hx`
|
||||||
@@ -428,7 +465,8 @@ function renderPlayerControls (state) {
|
|||||||
</i>
|
</i>
|
||||||
<input
|
<input
|
||||||
class='volume-slider float-right'
|
class='volume-slider float-right'
|
||||||
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
|
type='range' min='0' max='1' step='0.05'
|
||||||
|
value=${volumeChanging !== false ? volumeChanging : volume}
|
||||||
onmousedown=${handleVolumeScrub}
|
onmousedown=${handleVolumeScrub}
|
||||||
onmouseup=${handleVolumeScrub}
|
onmouseup=${handleVolumeScrub}
|
||||||
onmousemove=${handleVolumeScrub}
|
onmousemove=${handleVolumeScrub}
|
||||||
@@ -438,9 +476,11 @@ function renderPlayerControls (state) {
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
// Show video playback progress
|
// Show video playback progress
|
||||||
|
var currentTimeStr = formatTime(state.playing.currentTime)
|
||||||
|
var durationStr = formatTime(state.playing.duration)
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<span class='time float-left'>
|
<span class='time float-left'>
|
||||||
${formatTime(state.playing.currentTime)} / ${formatTime(state.playing.duration)}
|
${currentTimeStr} / ${durationStr}
|
||||||
</span>
|
</span>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
module.exports = Preferences
|
module.exports = Preferences
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
var hx = require('../lib/hx')
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
var {dispatch} = require('../lib/dispatcher')
|
var {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
var remote = require('electron').remote
|
var remote = require('electron').remote
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
module.exports = UnsupportedMediaModal
|
module.exports = UnsupportedMediaModal
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
var hx = require('../lib/hx')
|
||||||
|
|
||||||
function UnsupportedMediaModal (state) {
|
function UnsupportedMediaModal (state) {
|
||||||
var err = state.modal.error
|
var err = state.modal.error
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
module.exports = UpdateAvailableModal
|
module.exports = UpdateAvailableModal
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var {dispatch} = require('../lib/dispatcher')
|
var {dispatch} = require('../lib/dispatcher')
|
||||||
|
var hx = require('../lib/hx')
|
||||||
|
|
||||||
function UpdateAvailableModal (state) {
|
function UpdateAvailableModal (state) {
|
||||||
return hx`
|
return hx`
|
||||||
|
|||||||
@@ -28,20 +28,13 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
|||||||
|
|
||||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||||
// client, as explained here: https://webtorrent.io/faq
|
// client, as explained here: https://webtorrent.io/faq
|
||||||
var client = window.client = new WebTorrent({
|
var client = new WebTorrent()
|
||||||
tracker: {
|
|
||||||
// HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome bug.
|
|
||||||
// Fixed in Chrome 51, so we can remove this hack once Electron updates Chrome.
|
|
||||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/353
|
|
||||||
wrtc: process.platform !== 'darwin'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// WebTorrent-to-HTTP streaming sever
|
// WebTorrent-to-HTTP streaming sever
|
||||||
var server = window.server = null
|
var server = null
|
||||||
|
|
||||||
// Used for diffing, so we only send progress updates when necessary
|
// Used for diffing, so we only send progress updates when necessary
|
||||||
var prevProgress = window.prevProgress = null
|
var prevProgress = null
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
@@ -180,7 +173,7 @@ function saveTorrentFile (torrentKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, save the .torrent file, under the app config folder
|
// 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) {
|
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||||
console.log('saved torrent file %s', torrentPath)
|
console.log('saved torrent file %s', torrentPath)
|
||||||
@@ -193,7 +186,7 @@ function saveTorrentFile (torrentKey) {
|
|||||||
// Checks whether we've already resolved a given infohash to a torrent file
|
// 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
|
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
||||||
function checkIfTorrentFileExists (infoHash, cb) {
|
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) {
|
fs.exists(torrentPath, function (exists) {
|
||||||
cb(torrentPath, exists)
|
cb(torrentPath, exists)
|
||||||
})
|
})
|
||||||
@@ -206,10 +199,10 @@ function generateTorrentPoster (torrentKey) {
|
|||||||
torrentPoster(torrent, function (err, buf, extension) {
|
torrentPoster(torrent, function (err, buf, extension) {
|
||||||
if (err) return console.log('error generating poster: %o', err)
|
if (err) return console.log('error generating poster: %o', err)
|
||||||
// save it for next time
|
// save it for next time
|
||||||
fs.mkdirp(config.CONFIG_POSTER_PATH, function (err) {
|
fs.mkdirp(config.POSTER_PATH, function (err) {
|
||||||
if (err) return console.log('error creating poster dir: %o', err)
|
if (err) return console.log('error creating poster dir: %o', err)
|
||||||
var posterFileName = torrent.infoHash + extension
|
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) {
|
fs.writeFile(posterFilePath, buf, function (err) {
|
||||||
if (err) return console.log('error saving poster: %o', err)
|
if (err) return console.log('error saving poster: %o', err)
|
||||||
// show the poster
|
// show the poster
|
||||||
@@ -277,7 +270,7 @@ function getTorrentProgress () {
|
|||||||
function startServer (infoHash, index) {
|
function startServer (infoHash, index) {
|
||||||
var torrent = client.get(infoHash)
|
var torrent = client.get(infoHash)
|
||||||
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
|
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) {
|
function startServerFromReadyTorrent (torrent, index, cb) {
|
||||||
|
|||||||
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