Compare commits

...

77 Commits

Author SHA1 Message Date
Feross Aboukhadijeh
9d3e26f15a 0.7.2 2016-06-02 22:46:10 -07:00
Feross Aboukhadijeh
8a95895254 Ensure state.saved.prefs exists
Fixes a bug I introduced in the migrations
2016-06-02 22:33:22 -07:00
Feross Aboukhadijeh
5d71f9e9c6 code cleanup 2016-06-02 22:32:01 -07:00
Feross Aboukhadijeh
5d410457ce Merge pull request #622 from feross/about
Fix title in About Window
2016-06-02 21:12:58 -07:00
Feross Aboukhadijeh
c6cd21b8ff Fix title in About Window
Shows up as 'Electron' on Windows
2016-06-02 20:50:04 -07:00
Feross Aboukhadijeh
2235b2fa82 changelog 2016-06-02 20:10:40 -07:00
Feross Aboukhadijeh
65e0b5d6e7 0.7.1 2016-06-02 19:58:43 -07:00
Feross Aboukhadijeh
ea64411570 Merge pull request #621 from feross/f/fix
Fix v0.7 bug - refactor state
2016-06-02 19:53:53 -07:00
Feross Aboukhadijeh
9348c61a84 stray console.log 2016-06-02 19:53:37 -07:00
Feross Aboukhadijeh
d9aa3822ee Set selections by default
In case the user tries to change a file selection state before enabling
the torrent.
2016-06-02 19:52:35 -07:00
Feross Aboukhadijeh
e86bd26800 Refactor state save/load
- Fix bug where new install was relying on the migration to run on
startup to fix up the default config
- Moved save/load functions into state.js
- Removed exported getInitialState, getDefaultSavedState since that's
leaky. The state module should take care of that.
2016-06-02 19:46:29 -07:00
Feross Aboukhadijeh
6d8cec17de npm run open-config -- open folder 2016-06-02 19:40:12 -07:00
Feross Aboukhadijeh
572f084570 Merge pull request #620 from feross/skip-keys
Change step Forward/Backward shortcuts to match VLC's
2016-06-02 17:23:37 -07:00
Feross Aboukhadijeh
4a3ca5459d Change step Forward/Backward shortcuts to match VLC's
We got this feature idea from VLC, so let's align with the keyboard
shortcuts that they use :)

Closes #618.
2016-06-02 16:41:39 -07:00
Feross Aboukhadijeh
e8cb6abf0a 0.7.0 2016-06-02 16:21:56 -07:00
Feross Aboukhadijeh
94b3bc561d Packager: Move commands that can fail to post script 2016-06-02 13:50:36 -07:00
Feross Aboukhadijeh
5eb75d0250 authors 2016-06-02 13:48:07 -07:00
Feross Aboukhadijeh
b577e08053 changelog 2016-06-02 13:46:38 -07:00
Feross Aboukhadijeh
dae4840bd6 conditionally load migrations requires 2016-06-02 13:46:27 -07:00
Feross Aboukhadijeh
57eb52a606 Merge pull request #617 from feross/electron
electron-prebuilt@1.2.1
2016-06-01 19:28:48 -07:00
Feross Aboukhadijeh
6d670bdd3f electron-prebuilt@1.2.1 2016-06-01 19:19:20 -07:00
Feross Aboukhadijeh
def2209dc5 add todo comment 2016-06-01 19:18:36 -07:00
Feross Aboukhadijeh
763c573c7a Merge pull request #614 from feross/close-576
Fix subtitle controls radio button size
2016-06-01 19:17:13 -07:00
Feross Aboukhadijeh
eb61f2ac0e Merge pull request #613 from feross/fix-606
Fix IPC race condition in startup
2016-06-01 19:16:54 -07:00
Feross Aboukhadijeh
a9d1925686 Merge pull request #612 from feross/error-on-duplicate
Error on duplicate add (fixes to PR #593)
2016-06-01 19:15:34 -07:00
Feross Aboukhadijeh
0e10eba073 Merge pull request #611 from feross/close_caption
Use correct icon name
2016-06-01 19:14:41 -07:00
Feross Aboukhadijeh
0427e1f3a6 Fix subtitle controls radio button size
Closes #576.
2016-06-01 01:31:14 -07:00
gabriel
c841c94784 Preserve audio between sessions 2016-06-01 00:55:15 -07:00
gabriel
e3c6049fdb Do not use setVolume for storing global volume 2016-06-01 00:55:15 -07:00
gabriel
829206e921 Persist volume across videos 2016-06-01 00:55:15 -07:00
Feross Aboukhadijeh
f7acdffb2a Send ipcReady after state is initialized
Moving it sooner caused more than one bug, including
https://github.com/feross/webtorrent-desktop/issues/606 most recently.

The app feels no slower to start (even though the command line shows a
250ms slower start).

Closes #606.
2016-06-01 00:27:26 -07:00
Feross Aboukhadijeh
cc9ba385bf Show main window before ipcReady
It doesn't flicker anymore if we remove this option, and the app feels
no slower.
2016-06-01 00:25:18 -07:00
Feross Aboukhadijeh
e88ddd648b Cleanup argument handling in main 2016-06-01 00:20:11 -07:00
Feross Aboukhadijeh
dac34541d6 on -> once 2016-06-01 00:10:26 -07:00
Feross Aboukhadijeh
52fb378fd5 Error on duplicate add 2016-05-31 23:00:02 -07:00
Feross Aboukhadijeh
8fc61a1c90 Merge pull request #593 from mathiasvr/duplicate
Adding duplicate torrent when stopped
2016-05-31 22:30:04 -07:00
Feross Aboukhadijeh
04691ed0da Merge pull request #610 from feross/migration
Semver-based migrations
2016-05-31 17:55:52 -07:00
Feross Aboukhadijeh
f9d4e5e077 Use correct icon name
https://github.com/feross/webtorrent-desktop/issues/467

The icon name is "closed_caption", not "closed_captions"
http://jsbin.com/fenejob/1/edit?html,output

The extra space we're seeing is the icon font rendering the 's', which
renders as nothing.
2016-05-31 17:12:09 -07:00
Feross Aboukhadijeh
4ee36f459f Fix exception caused by incorrect file ordering
See
https://github.com/feross/webtorrent-desktop/pull/604#issuecomment-22280
5214
2016-05-31 13:15:48 -07:00
Feross Aboukhadijeh
2c0de25423 Semver-based migrations
- Put migrations into a separate file: migrations.js
- Add semver, for more specific version-based migrations.
- Change `state.saved.version` to semver app version.
2016-05-31 13:04:39 -07:00
Feross Aboukhadijeh
c82bdbd39d Fixes for PR #607 2016-05-31 12:12:25 -07:00
Feross Aboukhadijeh
71b08304f2 Merge branch 'pr/607' 2016-05-31 11:55:04 -07:00
Feross Aboukhadijeh
3bb3cd7c44 Merge pull request #609 from mathiasvr/menu-items
Fix 'fullscreen' and 'always on top' menu items
2016-05-31 11:41:21 -07:00
Mathias Rasmussen
41187ec43d Fix 'fullscreen' and 'always on top' menu items 2016-05-31 18:11:50 +02:00
Feross Aboukhadijeh
cf5de49deb Fix gray screen crash on config with defaultPlayFileIndex 2016-05-30 19:51:30 -07:00
Feross Aboukhadijeh
19f177f3ee remove console.log 2016-05-30 19:51:06 -07:00
Mathias Rasmussen
556d0cb1c5 Make add button accept non .torrent files 2016-05-31 03:04:02 +02:00
Mathias Rasmussen
7c7780b17e Adding duplicate torrent when stopped 2016-05-30 23:25:52 +02:00
Feross Aboukhadijeh
bd358b7692 Add comments to squirrel-win32.js 2016-05-30 02:30:20 -07:00
Feross Aboukhadijeh
1b8f180255 Merge pull request #604 from demoneaux/fix-file-sorting
Fix issue with file sorting.
2016-05-30 02:30:05 -07:00
Feross Aboukhadijeh
0bc90cea21 Merge pull request #605 from demoneaux/align-details
Align loading torrent message with torrent name.
2016-05-30 02:21:41 -07:00
Benjamin Tan
10f96ab23e Fix issue with file sorting.
Closes #591.
2016-05-30 13:18:38 +08:00
Benjamin Tan
4f0df507f4 Align loading torrent message with torrent name. 2016-05-30 13:17:42 +08:00
Feross Aboukhadijeh
256753e6ff Merge pull request #603 from demoneaux/fix-tray
Fix issue with tray on Windows.
2016-05-29 21:57:15 -07:00
Benjamin Tan
8ac42078d4 Fix issue with tray on Windows. 2016-05-30 11:29:53 +08:00
Feross Aboukhadijeh
fc83e054ea Merge pull request #597 from feross/refactor
[WIP] Major code cleanup
2016-05-29 04:06:51 -07:00
Feross Aboukhadijeh
62cb304971 Move unrelated code out of menu.js and ipc.js 2016-05-29 01:09:42 -07:00
Feross Aboukhadijeh
d4efebd694 Remove focusWindow function
Not needed -- win.show() automatically focuses, unminimizes, and shows
the window.
2016-05-28 19:10:55 -07:00
Feross Aboukhadijeh
7833f6bbc4 Dialogs on do not show a title on OS X, so the window title is used instead. 2016-05-28 19:09:05 -07:00
Feross Aboukhadijeh
8b773c5f59 Document and cleanup announcement/dialog/handlers.js 2016-05-28 18:25:25 -07:00
Feross Aboukhadijeh
5767d5b95d re-order view menu 2016-05-28 18:24:58 -07:00
Thomas Watson Steen
13f1ecdbe3 Use airplayer (#452)
* Use airplayer

* Mock AirPlay volume support

* Add AirPlay event support
2016-05-28 10:50:30 -07:00
Feross Aboukhadijeh
8ae4ac47e6 Perf: Temporarily disable dynamic subtitle detection
For
https://github.com/feross/webtorrent-desktop/pull/511#issuecomment-22119
8555
2016-05-27 18:51:42 -07:00
Feross Aboukhadijeh
001601bc5f Major refactor -- split windows into separate files 2016-05-27 00:01:30 -07:00
Feross Aboukhadijeh
3757507b18 cleanup announcement 2016-05-26 18:41:48 -07:00
Feross Aboukhadijeh
9abab7aec3 cleanup check-deps 2016-05-26 18:29:07 -07:00
Feross Aboukhadijeh
1aabd537d8 cleanup dispatcher 2016-05-26 18:12:23 -07:00
Feross Aboukhadijeh
6e240b3fd4 Misc file moving and cleanup
- Rename JS/CSS for main.html to be consistent (main.js, main.css)
- Add hx.js module to reduce virtual-dom boilerplate
- Move state.js into renderer/lib.js where it belongs
- Rename torrent-list.js -> home.js for consistency
- Rename create-torrent-page.js -> create-torrent.js for consistency
2016-05-26 17:47:16 -07:00
Feross Aboukhadijeh
501a07c386 Merge pull request #594 from feross/reenable-webrtc
Re-enable WebRTC (OS X, Windows)
2016-05-26 16:54:29 -07:00
Feross Aboukhadijeh
0d92dee14e style 2016-05-26 15:42:18 -07:00
Feross Aboukhadijeh
3a1fa25106 Re-enable WebRTC peers (OS X, Windows) 2016-05-26 15:42:12 -07:00
Feross Aboukhadijeh
b167770ea6 Merge pull request #587 from feross/electron-1.1.3
Electron 1.2.0
2016-05-26 15:39:02 -07:00
Feross Aboukhadijeh
2a8a26ac54 electron-prebuilt@1.2.0 2016-05-26 15:34:12 -07:00
Feross Aboukhadijeh
9748833ba9 Use .ico format on Windows
-  Use .ico format on Windows for best rendering quality, instead of
resized .png files
2016-05-26 15:33:33 -07:00
Feross Aboukhadijeh
bf49214790 electron-prebuilt@1.1.3 2016-05-26 15:33:33 -07:00
Feross Aboukhadijeh
2b4410a55a changelog 2016-05-26 15:26:23 -07:00
DC
bfd1b2eaf0 changelog 2016-05-26 04:00:38 -07:00
54 changed files with 1614 additions and 1295 deletions

View File

@@ -10,7 +10,7 @@
- Romain Beaumont <romain.rom1@gmail.com>
- Dan Flettre <fletd01@yahoo.com>
- Liam Gray <liam.r.gray@gmail.com>
- grunjol <grunjol@argenteam.net>
- grunjol <grunjol@users.noreply.github.com>
- Rémi Jouannet <remijouannet@users.noreply.github.com>
- Evan Miller <miller.evan815@gmail.com>
- Alex <alxmorais8@msn.com>
@@ -21,5 +21,6 @@
- Benjamin Tan <demoneaux@gmail.com>
- Mathias Rasmussen <mathiasvr@gmail.com>
- Sergey Bargamon <sergey@bargamon.ru>
- Thomas Watson Steen <w@tson.dk>
#### Generated by bin/update-authors.sh.

View File

@@ -1,15 +1,72 @@
# 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
### Added
- Added Preferences page
- Added Preferences page to set Download folder
- Save video position, resume playback from saved position
- Add additional video player keyboard shortcuts (#275)
- Use `poster.jpg` file as the poster image if available (#558)
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
- Add support for pasting a `instant.io` links (#559)
- Add support for pasting `instant.io` links (#559)
- Add announcement feature
### Changed

View File

@@ -3,49 +3,52 @@
var fs = require('fs')
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']
main()
// Scans our codebase and package.json for missing or unused dependencies
// Process returns 0 on success, prints a message and returns 1 on failure
// Scans codebase for missing or unused dependencies. Exits with code 0 on success.
function main () {
if (process.platform === 'win32') {
console.log('Sorry, check-deps only works on Mac and Linux')
console.error('Sorry, check-deps only works on Mac and Linux')
return
}
var jsDeps = findJSDeps()
var usedDeps = findUsedDeps()
var packageDeps = findPackageDeps()
var missingDeps = jsDeps.filter((dep) =>
packageDeps.indexOf(dep) < 0 &&
BUILT_IN_DEPS.indexOf(dep) < 0)
var unusedDeps = packageDeps.filter((dep) =>
jsDeps.indexOf(dep) < 0 &&
EXECUTABLE_DEPS.indexOf(dep) < 0)
var missingDeps = usedDeps.filter(
(dep) => !packageDeps.includes(dep) && !BUILT_IN_DEPS.includes(dep)
)
var unusedDeps = packageDeps.filter(
(dep) => !usedDeps.includes(dep) && !EXECUTABLE_DEPS.includes(dep)
)
if (missingDeps.length > 0) console.log('Missing package dependencies: ' + missingDeps)
if (unusedDeps.length > 0) console.log('Unused package dependencies: ' + unusedDeps)
if (missingDeps.length + unusedDeps.length > 0) process.exit(1)
console.log('Lookin good!')
if (missingDeps.length > 0) {
console.error('Missing package dependencies: ' + missingDeps)
}
if (unusedDeps.length > 0) {
console.error('Unused package dependencies: ' + unusedDeps)
}
if (missingDeps.length + unusedDeps.length > 0) {
process.exitCode = 1
}
}
// Finds all dependencies, required, optional, or dev, in package.json
// Finds all dependencies specified in `package.json`
function findPackageDeps () {
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
var requiredDeps = Object.keys(pkg.dependencies)
var deps = Object.keys(pkg.dependencies)
var devDeps = Object.keys(pkg.devDependencies)
var optionalDeps = Object.keys(pkg.optionalDependencies)
return [].concat(requiredDeps, devDeps, optionalDeps)
return [].concat(deps, devDeps, optionalDeps)
}
// Finds all dependencies required() in the code
function findJSDeps () {
// Finds all dependencies that used with `require()`
function findUsedDeps () {
var stdout = cp.execSync('./bin/list-deps.sh')
return stdout.toString().trim().split('\n')
}

View File

@@ -6,5 +6,5 @@ var path = require('path')
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
child.on('close', function (code) {
process.exit(code)
process.exitCode = code
})

View File

@@ -2,7 +2,5 @@
var config = require('../config')
var open = require('open')
var path = require('path')
var configPath = path.join(config.CONFIG_PATH, 'config.json')
open(configPath)
open(config.CONFIG_PATH)

View File

@@ -327,11 +327,11 @@ function buildDarwin (cb) {
}
var dmg = appDmg(dmgOpts)
dmg.on('error', cb)
dmg.once('error', cb)
dmg.on('progress', function (info) {
if (info.type === 'step-begin') console.log(info.title + '...')
})
dmg.on('finish', function (info) {
dmg.once('finish', function (info) {
console.log('OS X: Created dmg.')
cb(null)
})

View File

@@ -1,6 +1,7 @@
#!/bin/sh
set -e
npm run update-authors
git diff --exit-code
npm run package -- --sign
git push

View File

@@ -2,8 +2,6 @@
set -e
git pull
npm run update-authors
git diff --exit-code
rm -rf node_modules/
npm install
npm dedupe

View File

@@ -10,7 +10,6 @@ while (<>) {
next if $seen{$_};
next if /<support\@greenkeeper.io>/;
next if /<ungoldman\@gmail.com>/;
next if /<grunjol\@users.noreply.github.com>/;
next if /<dc\@DCs-MacBook.local>/;
next if /<rolandoguedes\@gmail.com>/;
$seen{$_} = push @authors, "- ", $_;

View File

@@ -24,11 +24,39 @@ module.exports = {
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
CONFIG_PATH: getConfigPath(),
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
DEFAULT_TORRENTS: [
{
name: 'Big Buck Bunny',
posterFileName: 'bigBuckBunny.jpg',
torrentFileName: 'bigBuckBunny.torrent'
},
{
name: 'Cosmos Laundromat (Preview)',
posterFileName: 'cosmosLaundromat.jpg',
torrentFileName: 'cosmosLaundromat.torrent'
},
{
name: 'Sintel',
posterFileName: 'sintel.jpg',
torrentFileName: 'sintel.torrent'
},
{
name: 'Tears of Steel',
posterFileName: 'tearsOfSteel.jpg',
torrentFileName: 'tearsOfSteel.torrent'
},
{
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
posterFileName: 'wiredCd.jpg',
torrentFileName: 'wiredCd.torrent'
}
],
DELAYED_INIT: 3000 /* 3 seconds */,
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
@@ -38,8 +66,10 @@ module.exports = {
IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(),
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: __dirname,
STATIC_PATH: path.join(__dirname, 'static'),
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
@@ -57,6 +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 () {
try {
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)

View File

@@ -3,7 +3,6 @@ module.exports = {
}
var electron = require('electron')
var get = require('simple-get')
var config = require('../config')
var log = require('./log')
@@ -12,27 +11,47 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform
/**
* In certain situations, the WebTorrent team may need to show an announcement to
* all WebTorrent Desktop users. For example: a security notice, or an update
* notification (if the auto-updater stops working).
*
* When there is an announcement, the `ANNOUNCEMENT_URL` endpoint should return an
* HTTP 200 status code with a JSON object like this:
*
* {
* "title": "WebTorrent Desktop Announcement",
* "message": "Security Issue in v0.xx",
* "detail": "Please update to v0.xx as soon as possible..."
* }
*/
function init () {
get.concat(ANNOUNCEMENT_URL, function (err, res, data) {
if (err) return log('failed to retrieve remote message')
if (res.statusCode !== 200) return log('no remote message')
try {
data = JSON.parse(data.toString())
} catch (err) {
data = {
title: 'WebTorrent Desktop Announcement',
message: 'WebTorrent Desktop Announcement',
detail: data.toString()
}
}
electron.dialog.showMessageBox({
type: 'info',
buttons: ['OK'],
title: data.title,
message: data.message,
detail: data.detail
}, function () {})
})
var get = require('simple-get')
get.concat(ANNOUNCEMENT_URL, onResponse)
}
function onResponse (err, res, data) {
if (err) return log(`Failed to retrieve announcement: ${err.message}`)
if (res.statusCode !== 200) return log('No announcement exists')
try {
data = JSON.parse(data.toString())
} catch (err) {
// Support plaintext announcement messages, using a default title.
data = {
title: 'WebTorrent Desktop Announcement',
message: data.toString(),
detail: data.toString()
}
}
electron.dialog.showMessageBox({
type: 'info',
buttons: ['OK'],
title: data.title,
message: data.message,
detail: data.detail
}, noop)
}
function noop () {}

122
main/dialog.js Normal file
View 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
View 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()
}
]
}

View File

@@ -3,9 +3,8 @@ module.exports = {
uninstall
}
var path = require('path')
var config = require('../config')
var path = require('path')
function install () {
if (process.platform === 'darwin') {
@@ -35,11 +34,11 @@ function installDarwin () {
var electron = require('electron')
var app = electron.app
// On OS X, only protocols that are listed in Info.plist can be set as the default
// handler at runtime.
// On OS X, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime.
app.setAsDefaultProtocolClient('magnet')
// File handlers are registered in the Info.plist.
// File handlers are defined in `Info.plist`.
}
function uninstallDarwin () {}
@@ -55,10 +54,22 @@ function installWin32 () {
var log = require('./log')
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND)
var iconPath = path.join(
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
)
registerProtocolHandlerWin32(
'magnet',
'URL:BitTorrent Magnet URL',
iconPath,
EXEC_COMMAND
)
registerFileHandlerWin32(
'.torrent',
'io.webtorrent.torrent',
'BitTorrent Document',
iconPath,
EXEC_COMMAND
)
/**
* To add a protocol handler, the following keys must be added to the Windows registry:
@@ -265,7 +276,9 @@ function installLinux () {
installIconFile()
function installDesktopFile () {
var templatePath = path.join(config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop')
var templatePath = path.join(
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
)
fs.readFile(templatePath, 'utf8', writeDesktopFile)
}

View File

@@ -8,6 +8,8 @@ var ipcMain = electron.ipcMain
var announcement = require('./announcement')
var config = require('../config')
var crashReporter = require('../crash-reporter')
var dialog = require('./dialog')
var dock = require('./dock')
var handlers = require('./handlers')
var ipc = require('./ipc')
var log = require('./log')
@@ -53,22 +55,22 @@ function init () {
ipc.init()
app.on('will-finish-launching', function () {
app.once('will-finish-launching', function () {
crashReporter.init()
})
app.on('ready', function () {
isReady = true
windows.createMainWindow()
windows.createWebTorrentHiddenWindow()
windows.main.init()
windows.webtorrent.init()
menu.init()
// To keep app startup fast, some code is delayed.
setTimeout(delayedInit, config.DELAYED_INIT)
})
app.on('ipcReady', function () {
app.once('ipcReady', function () {
log('Command line args:', argv)
processArgv(argv)
console.timeEnd('init')
@@ -79,20 +81,21 @@ function init () {
app.isQuitting = true
e.preventDefault()
windows.main.send('dispatch', 'saveState') /* try to save state on exit */
windows.main.dispatch('saveState') // try to save state on exit
ipcMain.once('savedState', () => app.quit())
setTimeout(() => app.quit(), 2000) /* quit after 2 secs, at most */
setTimeout(() => app.quit(), 2000) // quit after 2 secs, at most
})
app.on('activate', function () {
if (isReady) windows.createMainWindow()
if (isReady) windows.main.show()
})
}
function delayedInit () {
announcement.init()
tray.init()
dock.init()
handlers.install()
tray.init()
updater.init()
}
@@ -100,13 +103,12 @@ function onOpen (e, torrentId) {
e.preventDefault()
if (app.ipcReady) {
windows.main.send('dispatch', 'onOpen', torrentId)
// Magnet links opened from Chrome won't focus the app without a setTimeout. The
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
// Magnet links opened from Chrome won't focus the app without a setTimeout.
// The confirmation dialog Chrome shows causes Chrome to steal back the focus.
// Electron issue: https://github.com/atom/electron/issues/4338
setTimeout(function () {
windows.focusWindow(windows.main)
}, 100)
setTimeout(() => windows.main.show(), 100)
processArgv([ torrentId ])
} else {
argv.push(torrentId)
}
@@ -117,7 +119,7 @@ function onAppOpen (newArgv) {
if (app.ipcReady) {
log('Second app instance opened, but was prevented:', newArgv)
windows.focusWindow(windows.main)
windows.main.show()
processArgv(newArgv)
} else {
@@ -130,27 +132,22 @@ function sliceArgv (argv) {
}
function processArgv (argv) {
var pathsToOpen = []
var torrentIds = []
argv.forEach(function (arg) {
if (arg === '-n') {
menu.showOpenSeedFiles()
dialog.openSeedDirectory()
} else if (arg === '-o') {
menu.showOpenTorrentFile()
dialog.openTorrentFile()
} else if (arg === '-u') {
menu.showOpenTorrentAddress()
dialog.openTorrentAddress()
} else if (arg.startsWith('-psn')) {
// Ignore OS X launchd "process serial number" argument
// More: https://github.com/feross/webtorrent-desktop/issues/214
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
} else {
pathsToOpen.push(arg)
torrentIds.push(arg)
}
})
if (pathsToOpen.length > 0) openFilePaths(pathsToOpen)
}
// Send files to the renderer process
// Opening files means either adding torrents, creating and seeding a torrent
// from files, or adding subtitles
function openFilePaths (paths) {
windows.main.send('dispatch', 'onOpen', paths)
if (torrentIds.length > 0) {
windows.main.dispatch('onOpen', torrentIds)
}
}

View File

@@ -5,31 +5,32 @@ module.exports = {
var electron = require('electron')
var app = electron.app
var ipcMain = electron.ipcMain
var dialog = require('./dialog')
var dock = require('./dock')
var log = require('./log')
var menu = require('./menu')
var windows = require('./windows')
var powerSaveBlocker = require('./power-save-blocker')
var shell = require('./shell')
var shortcuts = require('./shortcuts')
var vlc = require('./vlc')
var windows = require('./windows')
// has to be a number, not a boolean, and undefined throws an error
var powerSaveBlockerId = 0
// messages from the main process, to be sent once the WebTorrent process starts
// Messages from the main process, to be sent once the WebTorrent process starts
var messageQueueMainToWebTorrent = []
// holds a ChildProcess while we're playing a video in VLC, null otherwise
var vlcProcess
function init () {
ipcMain.on('ipcReady', function (e) {
windows.main.show()
var ipc = electron.ipcMain
ipc.once('ipcReady', function (e) {
app.ipcReady = true
app.emit('ipcReady')
})
ipcMain.on('ipcReadyWebTorrent', function (e) {
ipc.once('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window',
messageQueueMainToWebTorrent.length)
@@ -39,113 +40,112 @@ function init () {
})
})
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
/**
* Dialog
*/
ipcMain.on('setBounds', function (e, bounds, maximize) {
setBounds(bounds, maximize)
})
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
ipc.on('openFiles', () => dialog.openFiles())
ipcMain.on('setAspectRatio', function (e, aspectRatio) {
setAspectRatio(aspectRatio)
})
/**
* Dock
*/
ipcMain.on('setBadge', function (e, text) {
setBadge(text)
})
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
ipcMain.on('setProgress', function (e, progress) {
setProgress(progress)
})
/**
* Events
*/
ipcMain.on('toggleFullScreen', function (e, flag) {
menu.toggleFullScreen(flag)
})
ipcMain.on('setTitle', function (e, title) {
windows.main.setTitle(title)
})
ipcMain.on('openItem', function (e, path) {
log('open item: ' + path)
electron.shell.openItem(path)
})
ipcMain.on('showItemInFolder', function (e, path) {
log('show item in folder: ' + path)
electron.shell.showItemInFolder(path)
})
ipcMain.on('blockPowerSave', blockPowerSave)
ipcMain.on('unblockPowerSave', unblockPowerSave)
ipcMain.on('onPlayerOpen', function () {
ipc.on('onPlayerOpen', function () {
menu.onPlayerOpen()
shortcuts.onPlayerOpen()
})
ipcMain.on('onPlayerClose', function () {
ipc.on('onPlayerClose', function () {
menu.onPlayerClose()
shortcuts.onPlayerOpen()
})
ipcMain.on('focusWindow', function (e, windowName) {
windows.focusWindow(windows[windowName])
})
/**
* Power Save Blocker
*/
ipcMain.on('downloadFinished', function (e, filePath) {
if (app.dock) {
// Bounces the Downloads stack if the filePath is inside the Downloads folder.
app.dock.downloadFinished(filePath)
}
})
ipc.on('blockPowerSave', () => powerSaveBlocker.start())
ipc.on('unblockPowerSave', () => powerSaveBlocker.stop())
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) {
windows.main.send('checkForVLC', isInstalled)
})
})
ipcMain.on('vlcPlay', function (e, url) {
ipc.on('vlcPlay', function (e, url) {
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url]
console.log('Running vlc ' + args.join(' '))
log('Running vlc ' + args.join(' '))
vlc.spawn(args, function (err, proc) {
if (err) return windows.main.send('dispatch', 'vlcNotFound')
if (err) return windows.main.dispatch('vlcNotFound')
vlcProcess = proc
// If it works, close the modal after a second
var closeModalTimeout = setTimeout(() =>
windows.main.send('dispatch', 'exitModal'), 1000)
windows.main.dispatch('exitModal'), 1000)
vlcProcess.on('close', function (code) {
clearTimeout(closeModalTimeout)
if (!vlcProcess) return // Killed
console.log('VLC exited with code ', code)
log('VLC exited with code ', code)
if (code === 0) {
windows.main.send('dispatch', 'backToList')
windows.main.dispatch('backToList')
} else {
windows.main.send('dispatch', 'vlcNotFound')
windows.main.dispatch('vlcNotFound')
}
vlcProcess = null
})
vlcProcess.on('error', function (e) {
console.log('VLC error', e)
log('VLC error', e)
})
})
})
ipcMain.on('vlcQuit', function () {
ipc.on('vlcQuit', function () {
if (!vlcProcess) return
console.log('Killing VLC, pid ' + vlcProcess.pid)
log('Killing VLC, pid ' + vlcProcess.pid)
vlcProcess.kill('SIGKILL') // kill -9
vlcProcess = null
})
// Capture all events
var oldEmit = ipcMain.emit
ipcMain.emit = function (name, e, ...args) {
var oldEmit = ipc.emit
ipc.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) {
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
@@ -168,82 +168,6 @@ function init () {
}
// Emit all other events normally
oldEmit.call(ipcMain, name, e, ...args)
}
}
function setBounds (bounds, maximize) {
// Do nothing in fullscreen
if (!windows.main || windows.main.isFullScreen()) {
log('setBounds: not setting bounds because we\'re in full screen')
return
}
// Maximize or minimize, if the second argument is present
var willBeMaximized
if (maximize === true) {
if (!windows.main.isMaximized()) {
log('setBounds: maximizing')
windows.main.maximize()
}
willBeMaximized = true
} else if (maximize === false) {
if (windows.main.isMaximized()) {
log('setBounds: unmaximizing')
windows.main.unmaximize()
}
willBeMaximized = false
} else {
willBeMaximized = windows.main.isMaximized()
}
// Assuming we're not maximized or maximizing, set the window size
if (!willBeMaximized) {
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen
var scr = electron.screen.getDisplayMatching(windows.main.getBounds())
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
log('setBounds: centered to ' + JSON.stringify(bounds))
}
windows.main.setBounds(bounds, true)
} else {
log('setBounds: not setting bounds because of window maximization')
}
}
function setAspectRatio (aspectRatio) {
log('setAspectRatio %o', aspectRatio)
if (windows.main) {
windows.main.setAspectRatio(aspectRatio)
}
}
// Display string in dock badging area (OS X)
function setBadge (text) {
log('setBadge %s', text)
if (app.dock) {
app.dock.setBadge(String(text))
}
}
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
function setProgress (progress) {
log('setProgress %s', progress)
if (windows.main) {
windows.main.setProgressBar(progress)
}
}
function blockPowerSave () {
powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep')
log('blockPowerSave %d', powerSaveBlockerId)
}
function unblockPowerSave () {
if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) {
electron.powerSaveBlocker.stop(powerSaveBlockerId)
log('unblockPowerSave %d', powerSaveBlockerId)
oldEmit.call(ipc, name, e, ...args)
}
}

View File

@@ -9,7 +9,6 @@ module.exports.error = error
*/
var electron = require('electron')
var windows = require('./windows')
var app = electron.app
@@ -18,7 +17,7 @@ function log (...args) {
if (app.ipcReady) {
windows.main.send('log', ...args)
} else {
app.on('ipcReady', () => windows.main.send('log', ...args))
app.once('ipcReady', () => windows.main.send('log', ...args))
}
}
@@ -26,6 +25,6 @@ function error (...args) {
if (app.ipcReady) {
windows.main.send('error', ...args)
} else {
app.on('ipcReady', () => windows.main.send('error', ...args))
app.once('ipcReady', () => windows.main.send('error', ...args))
}
}

View File

@@ -2,15 +2,10 @@ module.exports = {
init,
onPlayerClose,
onPlayerOpen,
onToggleAlwaysOnTop,
onToggleFullScreen,
onWindowHide,
onWindowShow,
// TODO: move these out of menu.js -- they don't belong here
showOpenSeedFiles,
showOpenTorrentAddress,
showOpenTorrentFile,
toggleFullScreen
onWindowBlur,
onWindowFocus
}
var electron = require('electron')
@@ -18,213 +13,67 @@ var electron = require('electron')
var app = electron.app
var config = require('../config')
var log = require('./log')
var dialog = require('./dialog')
var shell = require('./shell')
var windows = require('./windows')
var appMenu
var menu
function init () {
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
electron.Menu.setApplicationMenu(appMenu)
if (app.dock) {
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
app.dock.setMenu(dockMenu)
}
}
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 () {
if (windows.main) {
windows.main.send('dispatch', 'preferences')
}
}
function escapeBack () {
if (windows.main) {
windows.main.send('dispatch', 'escapeBack')
}
}
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
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(menu)
}
function onPlayerClose () {
log('onPlayerClose')
getMenuItem('Play/Pause').enabled = false
getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false
getMenuItem('Add Subtitles File...').enabled = false
getMenuItem('Step Forward').enabled = false
getMenuItem('Step Backward').enabled = false
getMenuItem('Increase Speed').enabled = false
getMenuItem('Decrease Speed').enabled = false
getMenuItem('Add Subtitles File...').enabled = false
}
function onToggleFullScreen (isFullScreen) {
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen()
windows.main.setMenuBarVisibility(!isFullScreen)
getMenuItem('Full Screen').checked = isFullScreen
windows.main.send('fullscreenChanged', isFullScreen)
function onPlayerOpen () {
getMenuItem('Play/Pause').enabled = true
getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true
getMenuItem('Step Forward').enabled = true
getMenuItem('Step Backward').enabled = true
getMenuItem('Increase Speed').enabled = true
getMenuItem('Decrease Speed').enabled = true
getMenuItem('Add Subtitles File...').enabled = true
}
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) {
for (var i = 0; i < appMenu.items.length; i++) {
var menuItem = appMenu.items[i].submenu.items.find(function (item) {
for (var i = 0; i < menu.items.length; i++) {
var menuItem = menu.items[i].submenu.items.find(function (item) {
return item.label === label
})
if (menuItem) return menuItem
}
}
// Prompts the user for a file, then creates a torrent. Only allows a single file
// selection.
function showOpenSeedFile () {
electron.dialog.showOpenDialog({
title: 'Select a file for the torrent file.',
properties: [ 'openFile' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
windows.main.send('dispatch', 'showCreateTorrent', selectedPaths)
})
}
// 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
windows.main.send('dispatch', 'showCreateTorrent', selectedPaths)
})
}
// Prompts the user to choose a torrent file, then adds it to the app
function showOpenTorrentFile () {
electron.dialog.showOpenDialog(windows.main, {
title: 'Select a .torrent file to open.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(function (selectedPath) {
windows.main.send('dispatch', 'addTorrent', selectedPath)
})
})
}
// Prompts the user for the URL of a torrent file, then downloads and adds it
function showOpenTorrentAddress () {
windows.main.send('showOpenTorrentAddress')
}
function getAppMenuTemplate () {
function getMenuTemplate () {
var template = [
{
label: 'File',
@@ -234,17 +83,17 @@ function getAppMenuTemplate () {
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles
click: () => dialog.openSeedDirectory()
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
click: () => dialog.openTorrentFile()
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
click: () => dialog.openTorrentAddress()
},
{
type: 'separator'
@@ -287,7 +136,7 @@ function getAppMenuTemplate () {
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => showPreferences()
click: () => windows.main.dispatch('preferences')
}
]
},
@@ -300,12 +149,20 @@ function getAppMenuTemplate () {
accelerator: process.platform === 'darwin'
? 'Ctrl+Command+F'
: 'F11',
click: () => toggleFullScreen()
click: () => windows.main.toggleFullScreen()
},
{
label: 'Float on Top',
type: 'checkbox',
click: () => toggleFloatOnTop()
click: () => windows.main.toggleAlwaysOnTop()
},
{
type: 'separator'
},
{
label: 'Go Back',
accelerator: 'Esc',
click: () => windows.main.dispatch('escapeBack')
},
{
type: 'separator'
@@ -318,24 +175,16 @@ function getAppMenuTemplate () {
accelerator: process.platform === 'darwin'
? 'Alt+Command+I'
: 'Ctrl+Shift+I',
click: toggleDevTools
click: () => windows.main.toggleDevTools()
},
{
label: 'Show WebTorrent Process',
accelerator: process.platform === 'darwin'
? 'Alt+Command+P'
: 'Ctrl+Shift+P',
click: showWebTorrentWindow
click: () => windows.webtorrent.toggleDevTools()
}
]
},
{
type: 'separator'
},
{
label: 'Go Back',
accelerator: 'Esc',
click: escapeBack
}
]
},
@@ -345,7 +194,7 @@ function getAppMenuTemplate () {
{
label: 'Play/Pause',
accelerator: 'Space',
click: playPause,
click: () => windows.main.dispatch('playPause'),
enabled: false
},
{
@@ -354,13 +203,13 @@ function getAppMenuTemplate () {
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
click: () => windows.main.dispatch('changeVolume', 0.1),
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
click: () => windows.main.dispatch('changeVolume', -0.1),
enabled: false
},
{
@@ -368,14 +217,18 @@ function getAppMenuTemplate () {
},
{
label: 'Step Forward',
accelerator: 'CmdOrCtrl+Alt+Right',
click: skipForward,
accelerator: process.platform === 'darwin'
? 'CmdOrCtrl+Alt+Right'
: 'Alt+Right',
click: () => windows.main.dispatch('skip', 1),
enabled: false
},
{
label: 'Step Backward',
accelerator: 'CmdOrCtrl+Alt+Left',
click: skipBack,
accelerator: process.platform === 'darwin'
? 'CmdOrCtrl+Alt+Left'
: 'Alt+Left',
click: () => windows.main.dispatch('skip', -1),
enabled: false
},
{
@@ -384,13 +237,13 @@ function getAppMenuTemplate () {
{
label: 'Increase Speed',
accelerator: 'CmdOrCtrl+=',
click: increasePlaybackRate,
click: () => windows.main.dispatch('changePlaybackRate', 1),
enabled: false
},
{
label: 'Decrease Speed',
accelerator: 'CmdOrCtrl+-',
click: decreasePlaybackRate,
click: () => windows.main.dispatch('changePlaybackRate', -1),
enabled: false
},
{
@@ -398,7 +251,7 @@ function getAppMenuTemplate () {
},
{
label: 'Add Subtitles File...',
click: openSubtitles,
click: () => windows.main.dispatch('openSubtitles'),
enabled: false
}
]
@@ -409,18 +262,18 @@ function getAppMenuTemplate () {
submenu: [
{
label: 'Learn more about ' + config.APP_NAME,
click: () => electron.shell.openExternal(config.HOME_PAGE_URL)
click: () => shell.openExternal(config.HOME_PAGE_URL)
},
{
label: 'Contribute on GitHub',
click: () => electron.shell.openExternal(config.GITHUB_URL)
click: () => shell.openExternal(config.GITHUB_URL)
},
{
type: 'separator'
},
{
label: 'Report an Issue...',
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES)
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
}
]
}
@@ -441,7 +294,7 @@ function getAppMenuTemplate () {
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => showPreferences()
click: () => windows.main.dispatch('preferences')
},
{
type: 'separator'
@@ -500,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') {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: showOpenSeedFile
click: () => dialog.openSeedFile()
})
// Help menu (Windows, Linux)
@@ -515,12 +369,12 @@ function getAppMenuTemplate () {
},
{
label: 'About ' + config.APP_NAME,
click: windows.createAboutWindow
click: () => windows.about.init()
}
)
}
// Add "File > Quit" menu item so Linux distros where the system tray icon is missing
// will have a way to quit the app.
// Add "File > Quit" menu item so Linux distros where the system tray icon is
// missing will have a way to quit the app.
if (process.platform === 'linux') {
// File menu (Linux)
template[0].submenu.push({
@@ -531,23 +385,3 @@ function getAppMenuTemplate () {
return template
}
function getDockMenuTemplate () {
return [
{
label: 'Create New Torrent...',
accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
}
]
}

View 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
View 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 desktops default manner.
*/
function openExternal (url) {
log(`openExternal: ${url}`)
electron.shell.openExternal(url)
}
/**
* Open the given file in the desktops 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)
}

View File

@@ -7,13 +7,14 @@ var electron = require('electron')
var windows = require('./windows')
function onPlayerOpen () {
// Register special "media key" for play/pause, available on some keyboards
// Register play/pause media key, available on some keyboards.
electron.globalShortcut.register(
'MediaPlayPause',
() => windows.main.send('dispatch', 'playPause')
() => windows.main.dispatch('playPause')
)
}
function onPlayerClose () {
// Return the media key to the OS, so other apps can use it.
electron.globalShortcut.unregister('MediaPlayPause')
}

View File

@@ -12,8 +12,8 @@ var app = electron.app
var handlers = require('./handlers')
var exeName = path.basename(process.execPath)
var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe')
var EXE_NAME = path.basename(process.execPath)
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
function handleEvent (cmd) {
if (cmd === '--squirrel-install') {
@@ -61,15 +61,17 @@ function handleEvent (cmd) {
}
if (cmd === '--squirrel-firstrun') {
// This is called on the app's first run. Do not quit, allow startup to continue.
// App is running for the first time. Do not quit, allow startup to continue.
return false
}
return false
}
// Spawn a command and invoke the callback when it completes with an error and the output
// from standard out.
/**
* Spawn a command and invoke the callback when it completes with an error and
* the output from standard out.
*/
function spawn (command, args, cb) {
var stdout = ''
@@ -99,24 +101,31 @@ function spawn (command, args, cb) {
})
}
// Spawn Squirrel's Update.exe with the given arguments and invoke the callback when the
// command completes.
/**
* Spawn the Squirrel `Update.exe` command with the given arguments and invoke
* the callback when the command completes.
*/
function spawnUpdate (args, cb) {
spawn(updateDotExe, args, cb)
spawn(UPDATE_EXE, args, cb)
}
// Create desktop/start menu shortcuts using the Squirrel Update.exe command line API
/**
* Create desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function createShortcuts (cb) {
spawnUpdate(['--createShortcut', exeName], cb)
spawnUpdate(['--createShortcut', EXE_NAME], cb)
}
// Update desktop/start menu shortcuts using the Squirrel Update.exe command line API
/**
* Update desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function updateShortcuts (cb) {
var homeDir = os.homedir()
if (homeDir) {
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
// Check if the desktop shortcut has been previously deleted and and keep it deleted
// if it was
// If the desktop shortcut was deleted by the user, then keep it deleted.
fs.access(desktopShortcutPath, function (err) {
var desktopShortcutExists = !err
createShortcuts(function () {
@@ -133,7 +142,10 @@ function updateShortcuts (cb) {
}
}
// Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API
/**
* Remove desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function removeShortcuts (cb) {
spawnUpdate(['--removeShortcut', exeName], cb)
spawnUpdate(['--removeShortcut', EXE_NAME], cb)
}

View File

@@ -1,51 +1,62 @@
module.exports = {
hasTray,
init,
hasTray
onWindowBlur,
onWindowFocus
}
var cp = require('child_process')
var path = require('path')
var electron = require('electron')
var app = electron.app
var config = require('../config')
var windows = require('./windows')
var trayIcon
var tray
function init () {
// OS X has no tray icon
if (process.platform === 'darwin') return
// On Linux, asynchronously check for libappindicator1
if (process.platform === 'linux') {
checkLinuxTraySupport(function (supportsTray) {
if (supportsTray) createTrayIcon()
})
initLinux()
}
// Windows always supports minimize-to-tray
if (process.platform === 'win32') createTrayIcon()
if (process.platform === 'win32') {
initWin32()
}
// OS X apps generally do not have menu bar icons
}
/**
* Returns true if there a tray icon is active.
*/
function hasTray () {
return !!trayIcon
return !!tray
}
function createTrayIcon () {
trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
// On Windows, left click to open the app, right click for context menu
// On Linux, any click (right or left) opens the context menu
trayIcon.on('click', showApp)
// Show the tray context menu, and keep the available commands up to date
function onWindowBlur () {
if (!tray) return
updateTrayMenu()
windows.main.on('show', updateTrayMenu)
windows.main.on('hide', updateTrayMenu)
}
function onWindowFocus () {
if (!tray) return
updateTrayMenu()
}
function initLinux () {
checkLinuxTraySupport(function (supportsTray) {
if (supportsTray) createTray()
})
}
function initWin32 () {
createTray()
}
/**
* Check for libappindicator1 support before creating tray icon
*/
function checkLinuxTraySupport (cb) {
var cp = require('child_process')
// Check that we're on Ubuntu (or another debian system) and that we have
// libappindicator1. If WebTorrent was installed from the deb file, we should
// always have it. If it was installed from the zip file, we might not.
@@ -57,25 +68,48 @@ function checkLinuxTraySupport (cb) {
})
}
function createTray () {
tray = new electron.Tray(getIconPath())
// On Windows, left click opens the app, right click opens the context menu.
// On Linux, any click (left or right) opens the context menu.
tray.on('click', () => windows.main.show())
// Show the tray context menu, and keep the available commands up to date
updateTrayMenu()
}
function updateTrayMenu () {
var showHideMenuItem
if (windows.main.isVisible()) {
showHideMenuItem = { label: 'Hide to tray', click: hideApp }
} else {
showHideMenuItem = { label: 'Show', click: showApp }
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
tray.setContextMenu(contextMenu)
}
function getMenuTemplate () {
return [
getToggleItem(),
{
label: 'Quit',
click: () => app.quit()
}
]
function getToggleItem () {
if (windows.main.win.isVisible()) {
return {
label: 'Hide to tray',
click: () => windows.main.hide()
}
} else {
return {
label: 'Show WebTorrent',
click: () => windows.main.show()
}
}
}
var contextMenu = electron.Menu.buildFromTemplate([
showHideMenuItem,
{ label: 'Quit', click: () => app.quit() }
])
trayIcon.setContextMenu(contextMenu)
}
function showApp () {
windows.main.show()
}
function hideApp () {
windows.main.hide()
windows.main.send('dispatch', 'backToList')
function getIconPath () {
return process.platform === 'win32'
? config.APP_ICON + '.ico'
: config.APP_ICON + '.png'
}

View File

@@ -21,27 +21,27 @@ function init () {
}
}
// The Electron auto-updater does not support Linux yet, so manually check for updates and
// `show the user a modal notification.
// The Electron auto-updater does not support Linux yet, so manually check for
// updates and show the user a modal notification.
function initLinux () {
get.concat(AUTO_UPDATE_URL, onResponse)
}
function onResponse (err, res, data) {
if (err) return log(`Update error: ${err.message}`)
if (res.statusCode === 200) {
// Update available
try {
data = JSON.parse(data)
} catch (err) {
return log(`Update error: Invalid JSON response: ${err.message}`)
}
windows.main.send('dispatch', 'updateAvailable', data.version)
} else if (res.statusCode === 204) {
// No update available
} else {
// Unexpected status code
log(`Update error: Unexpected status code: ${res.statusCode}`)
function onResponse (err, res, data) {
if (err) return log(`Update error: ${err.message}`)
if (res.statusCode === 200) {
// Update available
try {
data = JSON.parse(data)
} catch (err) {
return log(`Update error: Invalid JSON response: ${err.message}`)
}
windows.main.dispatch('updateAvailable', data.version)
} else if (res.statusCode === 204) {
// No update available
} else {
// Unexpected status code
log(`Update error: Unexpected status code: ${res.statusCode}`)
}
}

View File

@@ -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
View 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
View File

@@ -0,0 +1,3 @@
exports.about = require('./about')
exports.main = require('./main')
exports.webtorrent = require('./webtorrent')

214
main/windows/main.js Normal file
View 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'
}

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

View File

@@ -1,7 +1,7 @@
{
"name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.6.1",
"version": "0.7.2",
"author": {
"name": "WebTorrent, LLC",
"email": "feross@feross.org",
@@ -14,7 +14,7 @@
"url": "https://github.com/feross/webtorrent-desktop/issues"
},
"dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js",
"airplayer": "^2.0.0",
"application-config": "^0.2.1",
"bitfield": "^1.0.2",
"chromecasts": "^1.8.0",
@@ -22,7 +22,7 @@
"deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0",
"drag-drop": "^2.11.0",
"electron-prebuilt": "1.1.1",
"electron-prebuilt": "1.2.1",
"fs-extra": "^0.27.0",
"hyperx": "^2.0.2",
"iso-639-1": "^1.2.1",
@@ -30,8 +30,10 @@
"main-loop": "^3.2.0",
"musicmetadata": "^2.0.2",
"network-address": "^1.1.0",
"parse-torrent": "^5.7.3",
"prettier-bytes": "^1.0.1",
"run-parallel": "^1.1.6",
"semver": "^5.1.0",
"simple-concat": "^1.0.0",
"simple-get": "^2.0.0",
"srt-to-vtt": "^1.1.1",

View File

@@ -12,7 +12,7 @@ module.exports = {
setRate
}
var airplay = require('airplay-js')
var airplayer = require('airplayer')()
var chromecasts = require('chromecasts')()
var dlnacasts = require('dlnacasts')()
@@ -41,10 +41,9 @@ function init (appState, callback) {
state.devices.dlna = dlnaPlayer(player)
})
var browser = airplay.createBrowser()
browser.on('deviceOn', function (player) {
airplayer.on('update', function (player) {
state.devices.airplay = airplayPlayer(player)
}).start()
})
}
// chromecast player implementation
@@ -129,13 +128,31 @@ function chromecastPlayer (player) {
// airplay player implementation
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 () {
player.play(state.server.networkURL, 0, function (res) {
if (res.statusCode !== 200) {
player.play(state.server.networkURL, function (err, res) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to AirPlay.'
message: 'Could not connect to AirPlay. ' + err.message
})
} else {
state.playing.location = 'airplay'
@@ -145,11 +162,11 @@ function airplayPlayer (player) {
}
function play (callback) {
player.rate(1, callback)
player.resume(callback)
}
function pause (callback) {
player.rate(0, callback)
player.pause(callback)
}
function stop (callback) {
@@ -157,13 +174,18 @@ function airplayPlayer (player) {
}
function status () {
player.status(function (status) {
state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume
// According to docs is in [-30 - 0] (db) range
// should be converted to [0 - 1] using (val / 30 + 1)
update()
player.playbackInfo(function (err, res, status) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to AirPlay. ' + err.message
})
} else {
state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position
update()
}
})
}
@@ -172,12 +194,13 @@ function airplayPlayer (player) {
}
function volume (volume, callback) {
// TODO remove line below once we can fetch the information in status update
// AirPlay doesn't support volume
// TODO: We should just disable the volume slider
state.playing.volume = volume
volume = (volume - 1) * 30
player.volume(volume, callback)
}
addEvents()
return {
player: player,
open: open,

View File

@@ -1,36 +1,39 @@
module.exports = {
setDispatch,
dispatch,
dispatcher
dispatcher,
setDispatch
}
// Memoize most of our event handlers, which are functions in the form
// () => dispatch(<args>)
// ... this prevents virtual-dom from updating every listener on every update()
var _dispatchers = {}
var _dispatch = () => {}
var dispatchers = {}
var _dispatch = function () {}
function setDispatch (dispatch) {
_dispatch = dispatch
}
// Get a _memoized event handler that calls dispatch()
// All args must be JSON-able
function dispatch (...args) {
_dispatch(...args)
}
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler
// function. This prevents virtual-dom from updating the listener functions on
// each update().
function dispatcher (...args) {
var json = JSON.stringify(args)
var handler = _dispatchers[json]
var str = JSON.stringify(args)
var handler = dispatchers[str]
if (!handler) {
handler = _dispatchers[json] = (e) => {
// Don't click on whatever is below the button
handler = dispatchers[str] = function (e) {
// Do not propagate click to elements below the button
e.stopPropagation()
// Don't regisiter clicks on disabled buttons
if (e.currentTarget.classList.contains('disabled')) return
_dispatch.apply(null, args)
if (e.currentTarget.classList.contains('disabled')) {
// Ignore clicks on disabled elements
return
}
dispatch(...args)
}
}
return handler
}
function dispatch (...args) {
_dispatch.apply(null, args)
}

5
renderer/lib/hx.js Normal file
View File

@@ -0,0 +1,5 @@
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
module.exports = hx

View 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
View 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')
})
}

View File

@@ -10,14 +10,14 @@ var config = require('../../config')
// Returns an absolute path to the torrent file, or null if unavailable
function getTorrentPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.torrentFileName) return null
return path.join(config.CONFIG_TORRENT_PATH, torrentSummary.torrentFileName)
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
}
// Expects a torrentSummary
// Returns an absolute path to the poster image, or null if unavailable
function getPosterPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.posterFileName) return null
var posterPath = path.join(config.CONFIG_POSTER_PATH, torrentSummary.posterFileName)
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
// Backslashes in URLS in CSS cause bizarre string encoding issues
return posterPath.replace(/\\/g, '/')

View File

@@ -65,7 +65,7 @@ table {
display: flex;
flex-flow: column;
background: rgb(40, 40, 40);
animation: fadein 1s;
animation: fadein 0.5s;
}
.app:not(.is-focused) {
@@ -280,36 +280,36 @@ table {
width: 100%;
}
.create-torrent-page {
.create-torrent {
padding: 10px 25px;
overflow: hidden;
}
.create-torrent-page .torrent-attribute {
.create-torrent .torrent-attribute {
white-space: nowrap;
}
.create-torrent-page .torrent-attribute>* {
.create-torrent .torrent-attribute>* {
display: inline-block;
}
.create-torrent-page .torrent-attribute label {
.create-torrent .torrent-attribute label {
width: 60px;
margin-right: 10px;
vertical-align: top;
}
.create-torrent-page .torrent-attribute>div {
.create-torrent .torrent-attribute>div {
width: calc(100% - 90px);
}
.create-torrent-page .torrent-attribute div {
.create-torrent .torrent-attribute div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.create-torrent-page .torrent-attribute textarea {
.create-torrent .torrent-attribute textarea {
width: calc(100% - 80px);
height: 80px;
color: #eee;
@@ -321,11 +321,11 @@ table {
padding: 4px 6px;
}
.create-torrent-page textarea.torrent-trackers {
.create-torrent textarea.torrent-trackers {
height: 200px;
}
.create-torrent-page input.torrent-is-private {
.create-torrent input.torrent-is-private {
width: initial;
margin: 0;
}
@@ -605,6 +605,10 @@ body.drag .app::after {
padding: 8em 0 20px 0;
}
.torrent-details .warning {
padding-left: 15px;
}
.torrent-details table {
width: 100%;
white-space: nowrap;
@@ -711,10 +715,7 @@ body.drag .app::after {
font-size: 22px;
opacity: 0.85;
/*
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
/* Make all icons have uniform spacing */
max-width: 23px;
overflow: hidden;
}
@@ -750,7 +751,7 @@ body.drag .app::after {
opacity: 0.8;
}
.player .controls .icon.closed-captions {
.player .controls .icon.closed-caption {
font-size: 26px;
margin-top: 6px;
}
@@ -818,7 +819,7 @@ body.drag .app::after {
transition-timing-function: ease-out;
}
.player .controls .closed-captions.active,
.player .controls .closed-caption.active,
.player .controls .device.active {
color: #9af;
}
@@ -847,6 +848,28 @@ body.drag .app::after {
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.
*/
@@ -889,29 +912,6 @@ video::-webkit-media-text-track-container {
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
*/

View File

@@ -3,9 +3,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="index.css" charset="utf-8">
<link rel="stylesheet" href="main.css" charset="utf-8">
</head>
<body>
<script async src="index.js"></script>
<script async src="main.js"></script>
</body>
</html>

View File

@@ -3,18 +3,8 @@ console.time('init')
var crashReporter = require('../crash-reporter')
crashReporter.init()
var electron = require('electron')
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
// Listen for messages from the main process
setupIpc()
var appConfig = require('application-config')('WebTorrent')
var dragDrop = require('drag-drop')
var electron = require('electron')
var fs = require('fs-extra')
var mainLoop = require('main-loop')
var parallel = require('run-parallel')
@@ -28,39 +18,29 @@ var App = require('./views/app')
var config = require('../config')
var errors = require('./lib/errors')
var sound = require('./lib/sound')
var State = require('./state')
var State = require('./lib/state')
var TorrentPlayer = require('./lib/torrent-player')
var TorrentSummary = require('./lib/torrent-summary')
var {setDispatch} = require('./lib/dispatcher')
setDispatch(dispatch)
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
var state, vdomLoop
// This dependency is the slowest-loading, so we lazy load it
var Cast = null
var vdomLoop
init()
var state = State.getInitialState()
state.location.go({ url: 'home' }) // Add first page to location history
function init () {
// All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app.
State.load(onState)
// All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app.
loadState(init)
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()
})
setDispatch(dispatch)
}
/**
@@ -68,9 +48,12 @@ function loadState (cb) {
* Connects to the torrent networks, sets up the UI and OS integrations like
* the dock icon and drag+drop.
*/
function init () {
// Clean up the freshly-loaded config file, which may be from an older version
cleanUpConfig()
function onState (err, _state) {
if (err) return onError(err)
state = _state
// Add first page to location history
state.location.go({ url: 'home' })
// Restart everything we were torrenting last time the app ran
resumeTorrents()
@@ -89,6 +72,9 @@ function init () {
})
document.body.appendChild(vdomLoop.target)
// Listen for messages from the main process
setupIpc()
// Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary
// a progress bar and to keep the cursor in sync when playing a video
@@ -105,9 +91,9 @@ function init () {
window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur)
// Done! Ideally we want to get here <100ms after the user clicks the app
sound.play('STARTUP')
// Done! Ideally we want to get here < 500ms after the user clicks the app
console.timeEnd('init')
}
@@ -116,60 +102,6 @@ function delayedInit () {
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.files) {
ts.selections = ts.files.map((x) => true)
}
})
}
// Lazily loads Chromecast and Airplay support
function lazyLoadCast () {
if (!Cast) {
@@ -192,7 +124,7 @@ function render (state) {
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
function update () {
showOrHidePlayerControls()
if (vdomLoop) vdomLoop.update(state)
vdomLoop.update(state)
updateElectron()
}
@@ -224,17 +156,24 @@ function dispatch (action, ...args) {
if (action === 'addTorrent') {
addTorrent(args[0] /* torrent */)
}
if (action === 'showOpenTorrentFile') {
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
if (action === 'openTorrentFile') {
ipcRenderer.send('openTorrentFile') /* open torrent file */
}
if (action === 'openFiles') {
ipcRenderer.send('openFiles') /* add files with dialog */
}
if (action === 'showCreateTorrent') {
showCreateTorrent(args[0] /* paths */)
}
if (action === 'openTorrentAddress') {
state.modal = { id: 'open-torrent-address-modal' }
update()
}
if (action === 'createTorrent') {
createTorrent(args[0] /* options */)
}
if (action === 'openFile') {
openFile(args[0] /* infoHash */, args[1] /* index */)
if (action === 'openItem') {
openItem(args[0] /* infoHash */, args[1] /* index */)
}
if (action === 'toggleTorrent') {
toggleTorrent(args[0] /* infoHash */)
@@ -375,7 +314,10 @@ function dispatch (action, ...args) {
saveStateThrottled()
}
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
@@ -501,18 +443,11 @@ function isCasting () {
}
function setupIpc () {
ipcRenderer.send('ipcReady')
ipcRenderer.on('log', (e, ...args) => console.log(...args))
ipcRenderer.on('error', (e, ...args) => console.error(...args))
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
ipcRenderer.on('showOpenTorrentAddress', function (e) {
state.modal = { id: 'open-torrent-address-modal' }
update()
})
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
state.window.isFullScreen = isFullScreen
if (!isFullScreen) {
@@ -534,13 +469,15 @@ function setupIpc () {
ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args))
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
ipcRenderer.send('ipcReady')
}
// Starts all torrents that aren't paused on program startup
function resumeTorrents () {
state.saved.torrents
.filter((x) => x.status !== 'paused')
.forEach((x) => startTorrentingSummary(x))
.filter((torrentSummary) => torrentSummary.status !== 'paused')
.forEach((torrentSummary) => startTorrentingSummary(torrentSummary))
}
// Updates a single property in the UNSAVED prefs
@@ -561,7 +498,8 @@ function updatePreferences (property, value) {
// All unsaved prefs take effect atomically, and are saved to config.json
function savePreferences () {
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
saveState()
State.save(state)
update()
}
// Don't write state.saved to file more than once a second
@@ -569,44 +507,13 @@ function saveStateThrottled () {
if (state.saveStateTimeout) return
state.saveStateTimeout = setTimeout(function () {
delete state.saveStateTimeout
saveState()
State.save(state)
update()
}, 1000)
}
// Write state.saved to the JSON state file
function saveState () {
console.log('saving state to ' + appConfig.filePath)
// Clean up, so that we're not saving any pending state
var copy = Object.assign({}, state.saved)
// Remove torrents pending addition to the list, where we haven't finished
// reading the torrent file or file(s) to seed & don't have an infohash
copy.torrents = copy.torrents
.filter((x) => x.infoHash)
.map(function (x) {
var torrent = {}
for (var key in x) {
if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process
}
if (key === 'playStatus') {
continue // Don't save whether a torrent is playing / pending
}
torrent[key] = x[key]
}
return torrent
})
appConfig.write(copy, function (err) {
if (err) console.error(err)
ipcRenderer.send('savedState')
})
// Update right away, don't wait for the state to save
update()
}
// Called when the user drag-drops files onto the app
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
// via any method (drag-drop, drag to app icon, command line)
function onOpen (files) {
if (!Array.isArray(files)) files = [ files ]
@@ -667,6 +574,7 @@ function addTorrent (torrentId) {
torrentId = torrentId.path
}
// Allow a instant.io link to be pasted
// TODO: remove this once support is added to webtorrent core
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
}
@@ -674,12 +582,13 @@ function addTorrent (torrentId) {
}
function addSubtitles (files, autoSelect) {
// Subtitles are only supported while playing video
// Subtitles are only supported when playing video files
if (state.playing.type !== 'video') return
if (files.length === 0) return
// Read the files concurrently, then add all resulting subtitle tracks
var jobs = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(jobs, function (err, tracks) {
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(tasks, function (err, tracks) {
if (err) return onError(err)
for (var i = 0; i < tracks.length; i++) {
@@ -874,6 +783,12 @@ function torrentInfoHash (torrentKey, infoHash) {
torrentSummary ? 'existing' : 'new', torrentKey)
if (!torrentSummary) {
// Check if an existing (non-active) torrent has the same info hash
if (state.saved.torrents.find((t) => t.infoHash === infoHash)) {
ipcRenderer.send('wt-stop-torrenting', infoHash)
return onError(new Error('Cannot add duplicate torrent'))
}
torrentSummary = {
torrentKey: torrentKey,
status: 'new'
@@ -974,7 +889,10 @@ function torrentProgress (progressInfo) {
torrentSummary.progress = p
})
checkForSubtitles()
// TODO: Find an efficient way to re-enable this line, which allows subtitle
// files which are completed after a video starts to play to be added
// dynamically to the list of subtitles.
// checkForSubtitles()
update()
}
@@ -1131,6 +1049,9 @@ function closePlayer (cb) {
ipcRenderer.send('vlcQuit')
}
state.window.title = config.APP_WINDOW_TITLE
// Lets save volume for later
state.previousVolume = state.playing.volume
state.playing = State.getDefaultPlayState()
state.server = null
@@ -1147,7 +1068,7 @@ function closePlayer (cb) {
cb()
}
function openFile (infoHash, index) {
function openItem (infoHash, index) {
var torrentSummary = getTorrentSummary(infoHash)
var filePath = path.join(
torrentSummary.path,
@@ -1312,7 +1233,7 @@ function showDoneNotification (torrent) {
})
notif.onclick = function () {
ipcRenderer.send('focusWindow', 'main')
ipcRenderer.send('show')
}
sound.play('DONE')

View File

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

View File

@@ -1,16 +1,15 @@
module.exports = App
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var hx = require('../lib/hx')
var Header = require('./header')
var Views = {
'home': require('./torrent-list'),
'home': require('./home'),
'player': require('./player'),
'create-torrent': require('./create-torrent-page'),
'create-torrent': require('./create-torrent'),
'preferences': require('./preferences')
}
var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal'),
'update-available-modal': require('./update-available-modal'),

View File

@@ -1,14 +1,11 @@
module.exports = CreateTorrentPage
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var createTorrent = require('create-torrent')
var path = require('path')
var prettyBytes = require('prettier-bytes')
var {dispatch, dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function CreateTorrentPage (state) {
var info = state.location.current()
@@ -59,7 +56,7 @@ function CreateTorrentPage (state) {
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
return hx`
<div class='create-torrent-page'>
<div class='create-torrent'>
<h2>Create torrent ${defaultName}</h2>
<p class="torrent-info">
${torrentInfo}
@@ -132,7 +129,7 @@ function CreateTorrentPage (state) {
function CreateTorrentErrorPage () {
return hx`
<div class='create-torrent-page'>
<div class='create-torrent'>
<h2>Create torrent</h2>
<p class="torrent-info">
<p>

View File

@@ -1,10 +1,7 @@
module.exports = Header
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function Header (state) {
return hx`
@@ -42,7 +39,7 @@ function Header (state) {
<i
class='icon add'
title='Add torrent'
onclick=${dispatcher('showOpenTorrentFile')}>
onclick=${dispatcher('openFiles')}>
add
</i>
`

View File

@@ -1,17 +1,17 @@
module.exports = TorrentList
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var prettyBytes = require('prettier-bytes')
var hx = require('../lib/hx')
var TorrentSummary = require('../lib/torrent-summary')
var TorrentPlayer = require('../lib/torrent-player')
var {dispatcher} = require('../lib/dispatcher')
function TorrentList (state) {
var torrentRows = state.saved.torrents.map(
(torrentSummary) => renderTorrent(torrentSummary))
(torrentSummary) => renderTorrent(torrentSummary)
)
return hx`
<div class='torrent-list'>
${torrentRows}
@@ -20,11 +20,7 @@ function TorrentList (state) {
</div>
</div>`
// Renders a torrent in the torrent list
// Includes name, download status, play button, background image
// May be expanded for additional info, including the list of files inside
function renderTorrent (torrentSummary) {
// Get ephemeral data (like progress %) directly from the WebTorrent handle
var infoHash = torrentSummary.infoHash
var isSelected = infoHash && state.selectedInfoHash === infoHash
@@ -195,12 +191,13 @@ function TorrentList (state) {
} else {
// We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files
.map((file, index) => ({ file, index }))
.sort(function (a, b) {
if (a.name < b.name) return -1
if (b.name < a.name) return 1
if (a.file.name < b.file.name) return -1
if (b.file.name < a.file.name) return 1
return 0
})
.map((file, index) => renderFileRow(torrentSummary, file, index))
.map((object) => renderFileRow(torrentSummary, object.file, object.index))
filesElement = hx`
<div class='files'>
@@ -221,7 +218,8 @@ function TorrentList (state) {
// Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded
var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
// Are we even torrenting it?
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
var isDone = false // Are we finished torrenting it?
var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files) {
@@ -247,7 +245,7 @@ function TorrentList (state) {
handleClick = dispatcher('play', infoHash, index)
} else {
icon = 'description' /* file icon, opens in OS default app */
handleClick = dispatcher('openFile', infoHash, index)
handleClick = dispatcher('openItem', infoHash, index)
}
var rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented

View File

@@ -1,10 +1,7 @@
module.exports = OpenTorrentAddressModal
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatch} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function OpenTorrentAddressModal (state) {
return hx`

View File

@@ -1,13 +1,10 @@
module.exports = Player
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var Bitfield = require('bitfield')
var prettyBytes = require('prettier-bytes')
var zeroFill = require('zero-fill')
var hx = require('../lib/hx')
var TorrentSummary = require('../lib/torrent-summary')
var {dispatch, dispatcher} = require('../lib/dispatcher')
@@ -53,6 +50,12 @@ function renderMedia (state) {
if (state.playing.playbackRate !== mediaElement.playbackRate) {
mediaElement.playbackRate = state.playing.playbackRate
}
// Recover previous volume
if (state.previousVolume !== null && isFinite(state.previousVolume)) {
mediaElement.volume = state.previousVolume
state.previousVolume = null
}
// Set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
mediaElement.volume = state.playing.setVolume
@@ -60,7 +63,7 @@ function renderMedia (state) {
}
// Switch to the newly added subtitle track, if available
var tracks = mediaElement.textTracks
var tracks = mediaElement.textTracks || []
for (var j = 0; j < tracks.length; j++) {
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
@@ -366,10 +369,10 @@ function renderPlayerControls (state) {
if (state.playing.type === 'video') {
// show closed captions icon
elements.push(hx`
<i.icon.closed-captions.float-right
<i.icon.closed-caption.float-right
class=${captionsClass}
onclick=${handleSubtitles}>
closed_captions
closed_caption
</i>
`)
}

View File

@@ -1,8 +1,6 @@
module.exports = Preferences
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var hx = require('../lib/hx')
var {dispatch} = require('../lib/dispatcher')
var remote = require('electron').remote

View File

@@ -1,12 +1,9 @@
module.exports = UnsupportedMediaModal
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var electron = require('electron')
var {dispatch, dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function UnsupportedMediaModal (state) {
var err = state.modal.error

View File

@@ -1,12 +1,9 @@
module.exports = UpdateAvailableModal
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var electron = require('electron')
var {dispatch} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function UpdateAvailableModal (state) {
return hx`

View File

@@ -28,22 +28,13 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq
var client = window.client = new WebTorrent({
tracker: {
// HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome bug.
// Fixed in Chrome 51, so we can remove this hack once Electron updates Chrome.
// Issue: https://github.com/feross/webtorrent-desktop/issues/353
// HACK #2: Windows: Disable WebRTC to fix Chrome 50 / Electron 1.1.[1-3] crash.
// Issue: https://github.com/electron/electron/issues/5629
wrtc: process.platform === 'linux'
}
})
var client = new WebTorrent()
// WebTorrent-to-HTTP streaming sever
var server = window.server = null
var server = null
// Used for diffing, so we only send progress updates when necessary
var prevProgress = window.prevProgress = null
var prevProgress = null
init()
@@ -182,7 +173,7 @@ function saveTorrentFile (torrentKey) {
}
// Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) {
fs.mkdir(config.TORRENT_PATH, function (_) {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath)
@@ -195,7 +186,7 @@ function saveTorrentFile (torrentKey) {
// Checks whether we've already resolved a given infohash to a torrent file
// Calls back with (torrentPath, exists). Logs, does not call back on error
function checkIfTorrentFileExists (infoHash, cb) {
var torrentPath = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
fs.exists(torrentPath, function (exists) {
cb(torrentPath, exists)
})
@@ -208,10 +199,10 @@ function generateTorrentPoster (torrentKey) {
torrentPoster(torrent, function (err, buf, extension) {
if (err) return console.log('error generating poster: %o', err)
// save it for next time
fs.mkdirp(config.CONFIG_POSTER_PATH, function (err) {
fs.mkdirp(config.POSTER_PATH, function (err) {
if (err) return console.log('error creating poster dir: %o', err)
var posterFileName = torrent.infoHash + extension
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, posterFileName)
var posterFilePath = path.join(config.POSTER_PATH, posterFileName)
fs.writeFile(posterFilePath, buf, function (err) {
if (err) return console.log('error saving poster: %o', err)
// show the poster
@@ -279,7 +270,7 @@ function getTorrentProgress () {
function startServer (infoHash, index) {
var torrent = client.get(infoHash)
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
else torrent.on('ready', () => startServerFromReadyTorrent(torrent, index))
else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
}
function startServerFromReadyTorrent (torrent, index, cb) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB