Compare commits

...

38 Commits

Author SHA1 Message Date
DC
770327c3fa 0.8.1 2016-06-27 02:38:45 -07:00
Adam Gotlib
4bdc6e3d65 Fix typo in renderer/views/player.js (#673) 2016-06-27 00:28:19 -07:00
Feross Aboukhadijeh
4799a032e5 Fixes for PR #640 2016-06-23 18:57:08 -07:00
Feross Aboukhadijeh
b2d2a6a7a5 Merge pull request #640 from anonymlol/master
new protocol handler: stream-magnet
2016-06-23 18:37:53 -07:00
DC
7676106914 changelog 2016-06-23 07:45:02 -07:00
DC
fe5ea31f2c authors 2016-06-23 07:32:28 -07:00
DC
e34223fc94 0.8.0 2016-06-23 07:31:18 -07:00
Gediminas Petrikas
15f733f11c Windows Thumbnail Bar
* While in the player view, show a play/pause toggle in the thumbnail
2016-06-23 07:12:32 -07:00
DC
7526b18507 Show which cast device you're connected to 2016-06-23 07:09:49 -07:00
DC
0af6007632 Refactor cast menu 2016-06-23 07:09:49 -07:00
DC
1bc3cd1d51 Make check-deps handle older verions of node 2016-06-23 07:09:49 -07:00
DC
92bafd695d Listen to events on new cast devices 2016-06-23 07:09:49 -07:00
DC
78a2ee4e85 Cast menu
Fixes #301
2016-06-23 07:09:49 -07:00
Feross Aboukhadijeh
8b9346d767 Prevent playback continues after minimize (#662)
Fixes #649.
2016-06-23 06:59:55 -07:00
DC
06d3bd3f93 Seeding: sort files by path (#663)
Fixes a bug where you could create duplicate torrents by adding the same folder multiple times, because the file order & therefore the infohash was nondeterministic
2016-06-23 02:14:23 -07:00
Mathias Rasmussen
1af7e4ef19 Remove torrent data support (#641)
* add moveItemToTrash to shell

* delete torrent/data + context menu items
2016-06-22 18:58:16 -07:00
DC
8e64e4120b Telemetry: add Privacy section to README 2016-06-21 21:58:15 -07:00
DC
b983559763 Telemetry: address PR comments 2016-06-21 21:58:15 -07:00
DC
e62527de23 Telemetry: limit POST to 100kb 2016-06-21 04:20:12 -07:00
DC
1f51f35f8e Telemetry: report uncaught errors 2016-06-21 03:45:34 -07:00
DC
c3686417e3 Telemetry 2016-06-20 22:33:17 -07:00
Feross Aboukhadijeh
746e10c025 author email 2016-06-15 17:41:04 -07:00
Feross Aboukhadijeh
98389fc07c Merge pull request #644 from feross/dc/ux
Support .wmv video via VLC
2016-06-15 16:35:12 -07:00
DC
aaebf93db4 Support .wmv video via VLC. Fixes #625 2016-06-15 16:22:16 -07:00
anonymlol
8f03ecedaa fix 'isMagnet' is already defined error 2016-06-14 13:53:01 +02:00
anonymlol
db20bd8eaf New Handler: stream-magnet
only tested on windows
2016-06-14 13:30:38 +02:00
Feross Aboukhadijeh
12500dfb64 modals should stick to title bar 2016-06-13 16:15:45 -07:00
Feross Aboukhadijeh
acc8e7923a Update modal: improve buttons 2016-06-13 16:15:01 -07:00
Feross Aboukhadijeh
9aa5775528 Merge pull request #636 from mathiasvr/modals
fix modal inconsistencies
2016-06-13 16:08:05 -07:00
Mathias Rasmussen
2a2d71289a fix modal inconsistencies 2016-06-13 16:18:43 +02:00
Feross Aboukhadijeh
ae28e34fd5 Merge pull request #634 from feross/dc/fix
Make posters from jpeg files
2016-06-11 23:31:20 -07:00
DC
6b175e7d40 Make posters from jpeg files 2016-06-11 23:10:48 -07:00
Feross Aboukhadijeh
2c6d74e8ef Merge pull request #632 from mathiasvr/patch
handle play/pause when window is hidden
2016-06-10 20:23:58 -07:00
Mathias Rasmussen
3b832595fe handle play/pause when window is hidden
using `webkitvisibilitychange` event
2016-06-10 04:56:33 +02:00
Feross Aboukhadijeh
bf372029fb changelog 2016-06-03 15:10:11 -07:00
Feross Aboukhadijeh
17ce7e519c Merge pull request #623 from feross/tray
Fix Windows tray state
2016-06-02 23:24:33 -07:00
Feross Aboukhadijeh
1f6a112df7 changelog 2016-06-02 23:14:37 -07:00
Feross Aboukhadijeh
0ec6fb5a93 Fix Windows tray state
After this PR, the Windows tray state will be correct. "Show
WebTorrent" vs. "Hide WebTorrent"
2016-06-02 21:16:10 -07:00
28 changed files with 653 additions and 232 deletions

View File

@@ -22,5 +22,6 @@
- Mathias Rasmussen <mathiasvr@gmail.com> - Mathias Rasmussen <mathiasvr@gmail.com>
- Sergey Bargamon <sergey@bargamon.ru> - Sergey Bargamon <sergey@bargamon.ru>
- Thomas Watson Steen <w@tson.dk> - Thomas Watson Steen <w@tson.dk>
- Gediminas Petrikas <gedas18@gmail.com>
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,11 +1,37 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.8.0 - 2016-06-23
### Added
- Cast menu: choose which Chromecast, Airplay, or DLNA device you want to use
- Telemetry: send basic data, plus stats on how often the play button works
- Make posters from jpeg files, not just jpg
- Support .wmv video via Play in VLC
- Windows thumbnail bar with a play/pause button
### Changed
- Nicer modal styles
### Fixed
- Windows tray icon now stays in the right state
## v0.7.2 - 2016-06-02
### Fixed
- Fix exception that affects users upgrading from v0.5.1 or older
- Ensure `state.saved.prefs` configuration exists
- Fix window title on "About WebTorrent" window
## v0.7.1 - 2016-06-02 ## v0.7.1 - 2016-06-02
### Changed ### Changed
- Change "Step Forward" keyboard shortcut to `Alt+Left` - Change "Step Forward" keyboard shortcut to `Alt+Left` (Windows)
- Change "Step Backward" keyboard shortcut to to `Alt+Right` - Change "Step Backward" keyboard shortcut to to `Alt+Right` (Windows)
### Fixed ### Fixed

View File

@@ -81,6 +81,10 @@ brew install wine
(Requires the [Homebrew](http://brew.sh/) package manager.) (Requires the [Homebrew](http://brew.sh/) package manager.)
### Privacy
WebTorrent Desktop collects some basic usage stats to help us make the app better. For example, we track what OSs are users are on, and how well the play button works (how often does it succeed? time out? show a missing codec error?). The app never sends personally identifying or other private info.
### Code Style ### Code Style
[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)

View File

@@ -3,7 +3,48 @@
var fs = require('fs') var fs = require('fs')
var cp = require('child_process') var cp = require('child_process')
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path'] // We can't use `builtin-modules` here since our TravisCI
// setup expects this file to run with no dependencies
var BUILT_IN_NODE_MODULES = [
'assert',
'buffer',
'child_process',
'cluster',
'console',
'constants',
'crypto',
'dgram',
'dns',
'domain',
'events',
'fs',
'http',
'https',
'module',
'net',
'os',
'path',
'process',
'punycode',
'querystring',
'readline',
'repl',
'stream',
'string_decoder',
'timers',
'tls',
'tty',
'url',
'util',
'v8',
'vm',
'zlib'
]
var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
var EXECUTABLE_DEPS = ['gh-release', 'standard'] var EXECUTABLE_DEPS = ['gh-release', 'standard']
main() main()
@@ -19,10 +60,10 @@ function main () {
var packageDeps = findPackageDeps() var packageDeps = findPackageDeps()
var missingDeps = usedDeps.filter( var missingDeps = usedDeps.filter(
(dep) => !packageDeps.includes(dep) && !BUILT_IN_DEPS.includes(dep) (dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep)
) )
var unusedDeps = packageDeps.filter( var unusedDeps = packageDeps.filter(
(dep) => !usedDeps.includes(dep) && !EXECUTABLE_DEPS.includes(dep) (dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
) )
if (missingDeps.length > 0) { if (missingDeps.length > 0) {
@@ -52,3 +93,7 @@ function findUsedDeps () {
var stdout = cp.execSync('./bin/list-deps.sh') var stdout = cp.execSync('./bin/list-deps.sh')
return stdout.toString().trim().split('\n') return stdout.toString().trim().split('\n')
} }
function includes (arr, elem) {
return arr.indexOf(elem) >= 0
}

View File

@@ -206,6 +206,12 @@ function buildDarwin (cb) {
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns', CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
CFBundleURLName: 'BitTorrent Magnet URL', CFBundleURLName: 'BitTorrent Magnet URL',
CFBundleURLSchemes: [ 'magnet' ] CFBundleURLSchemes: [ 'magnet' ]
},
{
CFBundleTypeRole: 'Editor',
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
CFBundleURLName: 'BitTorrent Stream-Magnet URL',
CFBundleURLSchemes: [ 'stream-magnet' ]
} }
] ]

View File

@@ -10,6 +10,9 @@ var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings
module.exports = { module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement', ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
@@ -19,10 +22,6 @@ module.exports = {
APP_VERSION: APP_VERSION, APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)', APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
CONFIG_PATH: getConfigPath(), CONFIG_PATH: getConfigPath(),
DEFAULT_TORRENTS: [ DEFAULT_TORRENTS: [

View File

@@ -37,6 +37,7 @@ function installDarwin () {
// On OS X, only protocols that are listed in `Info.plist` can be set as the // On OS X, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime. // default handler at runtime.
app.setAsDefaultProtocolClient('magnet') app.setAsDefaultProtocolClient('magnet')
app.setAsDefaultProtocolClient('stream-magnet')
// File handlers are defined in `Info.plist`. // File handlers are defined in `Info.plist`.
} }
@@ -63,6 +64,12 @@ function installWin32 () {
iconPath, iconPath,
EXEC_COMMAND EXEC_COMMAND
) )
registerProtocolHandlerWin32(
'stream-magnet',
'URL:BitTorrent Stream-Magnet URL',
iconPath,
EXEC_COMMAND
)
registerFileHandlerWin32( registerFileHandlerWin32(
'.torrent', '.torrent',
'io.webtorrent.torrent', 'io.webtorrent.torrent',
@@ -201,6 +208,7 @@ function uninstallWin32 () {
var Registry = require('winreg') var Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND) unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
function unregisterProtocolHandlerWin32 (protocol, command) { function unregisterProtocolHandlerWin32 (protocol, command) {

View File

@@ -68,6 +68,13 @@ function init () {
// To keep app startup fast, some code is delayed. // To keep app startup fast, some code is delayed.
setTimeout(delayedInit, config.DELAYED_INIT) setTimeout(delayedInit, config.DELAYED_INIT)
// Report uncaught exceptions
process.on('uncaughtException', (err) => {
console.error(err)
var errJSON = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', errJSON)
})
}) })
app.once('ipcReady', function () { app.once('ipcReady', function () {

View File

@@ -15,6 +15,7 @@ var shell = require('./shell')
var shortcuts = require('./shortcuts') var shortcuts = require('./shortcuts')
var vlc = require('./vlc') var vlc = require('./vlc')
var windows = require('./windows') var windows = require('./windows')
var thumbnail = require('./thumbnail')
// 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 = [] var messageQueueMainToWebTorrent = []
@@ -68,6 +69,10 @@ function init () {
shortcuts.onPlayerOpen() shortcuts.onPlayerOpen()
}) })
ipc.on('updateThumbnailBar', function (e, isPaused) {
thumbnail.updateThumbarButtons(isPaused)
})
/** /**
* Power Save Blocker * Power Save Blocker
*/ */
@@ -81,6 +86,7 @@ function init () {
ipc.on('openItem', (e, ...args) => shell.openItem(...args)) ipc.on('openItem', (e, ...args) => shell.openItem(...args))
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args)) ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
/** /**
* Windows: Main * Windows: Main

View File

@@ -16,6 +16,7 @@ var config = require('../config')
var dialog = require('./dialog') var dialog = require('./dialog')
var shell = require('./shell') var shell = require('./shell')
var windows = require('./windows') var windows = require('./windows')
var thumbnail = require('./thumbnail')
var menu var menu
@@ -33,6 +34,8 @@ function onPlayerClose () {
getMenuItem('Increase Speed').enabled = false getMenuItem('Increase Speed').enabled = false
getMenuItem('Decrease Speed').enabled = false getMenuItem('Decrease Speed').enabled = false
getMenuItem('Add Subtitles File...').enabled = false getMenuItem('Add Subtitles File...').enabled = false
thumbnail.showPlayerThumbnailBar()
} }
function onPlayerOpen () { function onPlayerOpen () {
@@ -44,6 +47,8 @@ function onPlayerOpen () {
getMenuItem('Increase Speed').enabled = true getMenuItem('Increase Speed').enabled = true
getMenuItem('Decrease Speed').enabled = true getMenuItem('Decrease Speed').enabled = true
getMenuItem('Add Subtitles File...').enabled = true getMenuItem('Add Subtitles File...').enabled = true
thumbnail.hidePlayerThumbnailBar()
} }
function onToggleAlwaysOnTop (flag) { function onToggleAlwaysOnTop (flag) {

View File

@@ -1,7 +1,8 @@
module.exports = { module.exports = {
openExternal, openExternal,
openItem, openItem,
showItemInFolder showItemInFolder,
moveItemToTrash
} }
var electron = require('electron') var electron = require('electron')
@@ -30,3 +31,11 @@ function showItemInFolder (path) {
log(`showItemInFolder: ${path}`) log(`showItemInFolder: ${path}`)
electron.shell.showItemInFolder(path) electron.shell.showItemInFolder(path)
} }
/**
* Move the given file to trash and returns a boolean status for the operation.
*/
function moveItemToTrash (path) {
log(`moveItemToTrash: ${path}`)
electron.shell.moveItemToTrash(path)
}

35
main/thumbnail.js Normal file
View File

@@ -0,0 +1,35 @@
module.exports = {
showPlayerThumbnailBar,
hidePlayerThumbnailBar,
updateThumbarButtons
}
var path = require('path')
var config = require('../config')
var windows = require('./windows')
// gets called on player open
function showPlayerThumbnailBar () {
updateThumbarButtons(false)
}
// gets called on player close
function hidePlayerThumbnailBar () {
windows.main.win.setThumbarButtons([])
}
function updateThumbarButtons (isPaused) {
var icon = isPaused ? 'PlayThumbnailBarButton.png' : 'PauseThumbnailBarButton.png'
var tooltip = isPaused ? 'Play' : 'Pause'
var buttons = [
{
tooltip: tooltip,
icon: path.join(config.STATIC_PATH, icon),
click: function () {
windows.main.send('dispatch', 'playPause')
}
}
]
windows.main.win.setThumbarButtons(buttons)
}

View File

@@ -51,15 +51,11 @@ function init () {
menu.onToggleFullScreen(main.win.isFullScreen()) menu.onToggleFullScreen(main.win.isFullScreen())
}) })
win.on('blur', function () { win.on('blur', onWindowBlur)
menu.onWindowBlur() win.on('focus', onWindowFocus)
tray.onWindowBlur()
})
win.on('focus', function () { win.on('hide', onWindowBlur)
menu.onWindowFocus() win.on('show', onWindowFocus)
tray.onWindowFocus()
})
win.on('enter-full-screen', function () { win.on('enter-full-screen', function () {
menu.onToggleFullScreen(true) menu.onToggleFullScreen(true)
@@ -78,7 +74,7 @@ function init () {
app.quit() app.quit()
} else if (!app.isQuitting) { } else if (!app.isQuitting) {
e.preventDefault() e.preventDefault()
win.hide() hide()
} }
}) })
} }
@@ -207,6 +203,16 @@ function toggleFullScreen (flag) {
main.win.setFullScreen(flag) main.win.setFullScreen(flag)
} }
function onWindowBlur () {
menu.onWindowBlur()
tray.onWindowBlur()
}
function onWindowFocus () {
menu.onWindowFocus()
tray.onWindowFocus()
}
function getIconPath () { function getIconPath () {
return process.platform === 'win32' return process.platform === 'win32'
? config.APP_ICON + '.ico' ? config.APP_ICON + '.ico'

View File

@@ -1,10 +1,10 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.7.2", "version": "0.8.1",
"author": { "author": {
"name": "WebTorrent, LLC", "name": "WebTorrent, LLC",
"email": "feross@feross.org", "email": "feross@webtorrent.io",
"url": "https://webtorrent.io" "url": "https://webtorrent.io"
}, },
"bin": { "bin": {

View File

@@ -3,8 +3,9 @@
// * Starts and stops casting, provides remote video controls // * Starts and stops casting, provides remote video controls
module.exports = { module.exports = {
init, init,
open, toggleMenu,
close, selectDevice,
stop,
play, play,
pause, pause,
seek, seek,
@@ -32,24 +33,49 @@ function init (appState, callback) {
state = appState state = appState
update = callback update = callback
state.devices.chromecast = chromecastPlayer()
state.devices.dlna = dlnaPlayer()
state.devices.airplay = airplayPlayer()
// Listen for devices: Chromecast, DLNA and Airplay // Listen for devices: Chromecast, DLNA and Airplay
chromecasts.on('update', function (player) { chromecasts.on('update', function (device) {
state.devices.chromecast = chromecastPlayer(player) // TODO: how do we tell if there are *no longer* any Chromecasts available?
// From looking at the code, chromecasts.players only grows, never shrinks
state.devices.chromecast.addDevice(device)
}) })
dlnacasts.on('update', function (player) { dlnacasts.on('update', function (device) {
state.devices.dlna = dlnaPlayer(player) state.devices.dlna.addDevice(device)
}) })
airplayer.on('update', function (player) { airplayer.on('update', function (device) {
state.devices.airplay = airplayPlayer(player) state.devices.airplay.addDevice(device)
}) })
} }
// chromecast player implementation // chromecast player implementation
function chromecastPlayer (player) { function chromecastPlayer () {
function addEvents () { var ret = {
player.on('error', function (err) { device: null,
addDevice,
getDevices,
open,
play,
pause,
stop,
status,
seek,
volume
}
return ret
function getDevices () {
return chromecasts.players
}
function addDevice (device) {
device.on('error', function (err) {
if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
time: new Date().getTime(), time: new Date().getTime(),
@@ -57,7 +83,8 @@ function chromecastPlayer (player) {
}) })
update() update()
}) })
player.on('disconnect', function () { device.on('disconnect', function () {
if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
}) })
@@ -65,7 +92,7 @@ function chromecastPlayer (player) {
function open () { function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
player.play(state.server.networkURL, { ret.device.play(state.server.networkURL, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) { }, function (err) {
@@ -83,19 +110,19 @@ function chromecastPlayer (player) {
} }
function play (callback) { function play (callback) {
player.play(null, null, callback) ret.device.play(null, null, callback)
} }
function pause (callback) { function pause (callback) {
player.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
player.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
player.status(function (err, status) { ret.device.status(function (err, status) {
if (err) return console.log('error getting %s status: %o', state.playing.location, err) if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED' state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime state.playing.currentTime = status.currentTime
@@ -105,30 +132,31 @@ function chromecastPlayer (player) {
} }
function seek (time, callback) { function seek (time, callback) {
player.seek(time, callback) ret.device.seek(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
player.volume(volume, callback) ret.device.volume(volume, callback)
}
addEvents()
return {
player: player,
open: open,
play: play,
pause: pause,
stop: stop,
status: status,
seek: seek,
volume: volume
} }
} }
// airplay player implementation // airplay player implementation
function airplayPlayer (player) { function airplayPlayer () {
function addEvents () { var ret = {
device: null,
addDevice,
getDevices,
open,
play,
pause,
stop,
status,
seek,
volume
}
return ret
function addDevice (player) {
player.on('event', function (event) { player.on('event', function (event) {
switch (event.state) { switch (event.state) {
case 'loading': case 'loading':
@@ -146,8 +174,12 @@ function airplayPlayer (player) {
}) })
} }
function getDevices () {
return airplayer.players
}
function open () { function open () {
player.play(state.server.networkURL, function (err, res) { ret.device.play(state.server.networkURL, function (err, res) {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -162,19 +194,19 @@ function airplayPlayer (player) {
} }
function play (callback) { function play (callback) {
player.resume(callback) ret.device.resume(callback)
} }
function pause (callback) { function pause (callback) {
player.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
player.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
player.playbackInfo(function (err, res, status) { ret.device.playbackInfo(function (err, res, status) {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -190,7 +222,7 @@ function airplayPlayer (player) {
} }
function seek (time, callback) { function seek (time, callback) {
player.scrub(time, callback) ret.device.scrub(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
@@ -198,25 +230,31 @@ function airplayPlayer (player) {
// TODO: We should just disable the volume slider // TODO: We should just disable the volume slider
state.playing.volume = volume state.playing.volume = volume
} }
addEvents()
return {
player: player,
open: open,
play: play,
pause: pause,
stop: stop,
status: status,
seek: seek,
volume: volume
}
} }
// DLNA player implementation // DLNA player implementation
function dlnaPlayer (player) { function dlnaPlayer (player) {
function addEvents () { var ret = {
player.on('error', function (err) { device: null,
addDevice,
getDevices,
open,
play,
pause,
stop,
status,
seek,
volume
}
return ret
function getDevices () {
return dlnacasts.players
}
function addDevice (device) {
device.on('error', function (err) {
if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
time: new Date().getTime(), time: new Date().getTime(),
@@ -224,7 +262,8 @@ function dlnaPlayer (player) {
}) })
update() update()
}) })
player.on('disconnect', function () { device.on('disconnect', function () {
if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
}) })
@@ -232,7 +271,7 @@ function dlnaPlayer (player) {
function open () { function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
player.play(state.server.networkURL, { ret.device.play(state.server.networkURL, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0 seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
@@ -251,19 +290,19 @@ function dlnaPlayer (player) {
} }
function play (callback) { function play (callback) {
player.play(null, null, callback) ret.device.play(null, null, callback)
} }
function pause (callback) { function pause (callback) {
player.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
player.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
player.status(function (err, status) { ret.device.status(function (err, status) {
if (err) return console.log('error getting %s status: %o', state.playing.location, err) if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED' state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime state.playing.currentTime = status.currentTime
@@ -273,61 +312,78 @@ function dlnaPlayer (player) {
} }
function seek (time, callback) { function seek (time, callback) {
player.seek(time, callback) ret.device.seek(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
player.volume(volume, function (err) { ret.device.volume(volume, function (err) {
// quick volume update // quick volume update
state.playing.volume = volume state.playing.volume = volume
callback(err) callback(err)
}) })
} }
addEvents()
return {
player: player,
open: open,
play: play,
pause: pause,
stop: stop,
status: status,
seek: seek,
volume: volume
}
} }
// Start polling cast device state, whenever we're connected // Start polling cast device state, whenever we're connected
function startStatusInterval () { function startStatusInterval () {
statusInterval = setInterval(function () { statusInterval = setInterval(function () {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.status()
device.status()
}
}, 1000) }, 1000)
} }
function open (location) { /*
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
* The menu lists eg. all Chromecasts detected; the user can click one to cast.
* If the menu was already showing for that type, hides the menu.
*/
function toggleMenu (location) {
// If the menu is already showing, hide it
if (state.devices.castMenu && state.devices.castMenu.location === location) {
state.devices.castMenu = null
return
}
// Never cast to two devices at the same time
if (state.playing.location !== 'local') { if (state.playing.location !== 'local') {
throw new Error('You can\'t connect to ' + location + ' when already connected to another device') throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
} }
state.playing.location = location + '-pending' // Find all cast devices of the given type
var device = getDevice(location) var player = getPlayer(location)
if (device) { var devices = player ? player.getDevices() : []
getDevice(location).open() if (devices.length === 0) throw new Error('No ' + location + ' devices available')
startStatusInterval()
}
// Show a menu
state.devices.castMenu = {location, devices}
}
function selectDevice (index) {
var {location, devices} = state.devices.castMenu
// Start casting
var player = getPlayer(location)
player.device = devices[index]
player.open()
// Poll the casting device's status every few seconds
startStatusInterval()
// Show the Connecting... screen
state.devices.castMenu = null
state.playing.castName = devices[index].name
state.playing.location = location + '-pending'
update() update()
} }
// Stops casting, move video back to local screen // Stops casting, move video back to local screen
function close () { function stop () {
var device = getDevice() var player = getPlayer()
if (device) { if (player) {
device.stop(stoppedCasting) player.stop(function () {
player.device = null
stoppedCasting()
})
clearInterval(statusInterval) clearInterval(statusInterval)
} else { } else {
stoppedCasting() stoppedCasting()
@@ -340,8 +396,8 @@ function stoppedCasting () {
update() update()
} }
function getDevice (location) { function getPlayer (location) {
if (location && state.devices[location]) { if (location) {
return state.devices[location] return state.devices[location]
} else if (state.playing.location === 'chromecast') { } else if (state.playing.location === 'chromecast') {
return state.devices.chromecast return state.devices.chromecast
@@ -355,29 +411,25 @@ function getDevice (location) {
} }
function play () { function play () {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.play(castCallback)
device.play(castCallback)
}
} }
function pause () { function pause () {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.pause(castCallback)
device.pause(castCallback)
}
} }
function setRate (rate) { function setRate (rate) {
var device var player
var result = true var result = true
if (state.playing.location === 'chromecast') { if (state.playing.location === 'chromecast') {
// TODO find how to control playback rate on chromecast // TODO find how to control playback rate on chromecast
castCallback() castCallback()
result = false result = false
} else if (state.playing.location === 'airplay') { } else if (state.playing.location === 'airplay') {
device = state.devices.airplay player = state.devices.airplay
device.rate(rate, castCallback) player.rate(rate, castCallback)
} else { } else {
result = false result = false
} }
@@ -385,17 +437,13 @@ function setRate (rate) {
} }
function seek (time) { function seek (time) {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.seek(time, castCallback)
device.seek(time, castCallback)
}
} }
function setVolume (volume) { function setVolume (volume) {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.volume(volume, castCallback)
device.volume(volume, castCallback)
}
} }
function castCallback () { function castCallback () {

View File

@@ -32,10 +32,7 @@ function getDefaultState () {
}, },
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */ 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 */ playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
devices: { /* playback devices like Chromecast and AppleTV */ devices: {}, /* playback devices like Chromecast and AppleTV */
airplay: null, /* airplay client. finds and manages AppleTVs */
chromecast: null /* chromecast client. finds and manages Chromecasts */
},
dock: { dock: {
badge: 0, badge: 0,
progress: 0 progress: 0

140
renderer/lib/telemetry.js Normal file
View File

@@ -0,0 +1,140 @@
// Collects anonymous usage stats and uncaught errors
// Reports back so that we can improve WebTorrent Desktop
module.exports = {
init,
logUncaughtError,
logPlayAttempt
}
const crypto = require('crypto')
const electron = require('electron')
const https = require('https')
const os = require('os')
const url = require('url')
const config = require('../../config')
var telemetry
function init (state) {
telemetry = state.saved.telemetry
if (!telemetry) {
telemetry = state.saved.telemetry = createSummary()
reset()
}
var now = new Date()
telemetry.timestamp = now.toISOString()
telemetry.localTime = now.toTimeString()
telemetry.screens = getScreenInfo()
telemetry.system = getSystemInfo()
telemetry.approxNumTorrents = getApproxNumTorrents(state)
postToServer(telemetry)
}
function reset () {
telemetry.uncaughtErrors = []
telemetry.playAttempts = {
total: 0,
success: 0,
timeout: 0,
error: 0,
abandoned: 0
}
}
function postToServer () {
// Serialize the telemetry summary
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
// POST to our server
var options = url.parse(config.TELEMETRY_URL)
options.method = 'POST'
options.headers = {
'Content-Type': 'application/json',
'Content-Length': payload.length
}
var req = https.request(options, function (res) {
if (res.statusCode === 200) {
console.log('Successfully posted telemetry summary')
reset()
} else {
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
}
})
req.on('error', function (e) {
console.error('Couldn\'t post telemetry summary', e)
})
req.write(payload)
req.end()
}
// Creates a new telemetry summary. Gives the user a unique ID,
// collects screen resolution, etc
function createSummary () {
// Make a 256-bit random unique ID
var userID = crypto.randomBytes(32).toString('hex')
return { userID }
}
// Track screen resolution
function getScreenInfo () {
return electron.screen.getAllDisplays().map((screen) => ({
width: screen.size.width,
height: screen.size.height,
scaleFactor: screen.scaleFactor
}))
}
// Track basic system info like OS version and amount of RAM
function getSystemInfo () {
return {
osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(),
architecture: os.arch(),
totalMemoryMB: os.totalmem() / (1 << 20),
numCores: os.cpus().length
}
}
// Get the number of torrents, rounded to the nearest power of two
function getApproxNumTorrents (state) {
var exactNum = state.saved.torrents.length
if (exactNum === 0) return 0
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
var log2 = Math.log(exactNum) / Math.log(2)
return 1 << Math.round(log2)
}
// An uncaught error happened in the main process or in one of the windows
function logUncaughtError (procName, err) {
var message, stack
if (typeof err === 'string') {
message = err
stack = ''
} else {
message = err.message
stack = err.stack
}
// We need to POST the telemetry object, make sure it stays < 100kb
if (telemetry.uncaughtErrors.length > 20) return
if (message.length > 1000) message = message.substring(0, 1000)
if (stack.length > 1000) stack = stack.substring(0, 1000)
telemetry.uncaughtErrors.push({process: procName, message, stack})
}
// The user pressed play. It either worked, timed out, or showed the
// "Play in VLC" codec error
function logPlayAttempt (result) {
if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) {
return console.error('Unknown play attempt result', result)
}
var attempts = telemetry.playAttempts
attempts.total = (attempts.total || 0) + 1
attempts[result] = (attempts[result] || 0) + 1
}

View File

@@ -24,7 +24,8 @@ function isVideo (file) {
'.mp4', '.mp4',
'.mpg', '.mpg',
'.ogv', '.ogv',
'.webm' '.webm',
'.wmv'
].includes(ext) ].includes(ext)
} }

View File

@@ -16,7 +16,7 @@ function torrentPoster (torrent, cb) {
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb) if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
// Third, try to use the largest image file // Third, try to use the largest image file
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png']) var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb) if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
// TODO: generate a waveform from the largest sound file // TODO: generate a waveform from the largest sound file

View File

@@ -260,7 +260,7 @@ table {
.modal .modal-content { .modal .modal-content {
position: fixed; position: fixed;
top: 45px; top: 38px;
left: 0; left: 0;
right: 0; right: 0;
margin: 0 auto; margin: 0 auto;
@@ -848,7 +848,7 @@ body.drag .app::after {
height: 14px; height: 14px;
} }
.player .controls .subtitles-list { .player .controls .options-list {
position: fixed; position: fixed;
background: rgba(40, 40, 40, 0.8); background: rgba(40, 40, 40, 0.8);
min-width: 100px; min-width: 100px;
@@ -862,7 +862,7 @@ body.drag .app::after {
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
.player .controls .subtitles-list .icon { .player .controls .options-list .icon {
display: inline; display: inline;
font-size: 17px; font-size: 17px;
vertical-align: bottom; vertical-align: bottom;

View File

@@ -14,8 +14,9 @@ var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff') var diff = require('virtual-dom/diff')
var patch = require('virtual-dom/patch') var patch = require('virtual-dom/patch')
var App = require('./views/app')
var config = require('../config') var config = require('../config')
var App = require('./views/app')
var telemetry = require('./lib/telemetry')
var errors = require('./lib/errors') var errors = require('./lib/errors')
var sound = require('./lib/sound') var sound = require('./lib/sound')
var State = require('./lib/state') var State = require('./lib/state')
@@ -91,15 +92,22 @@ function onState (err, _state) {
window.addEventListener('focus', onFocus) window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur) window.addEventListener('blur', onBlur)
sound.play('STARTUP') // ...window visibility state.
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
// Log uncaught JS errors
window.addEventListener('error',
(e) => telemetry.logUncaughtError('window', e.error), true)
// Done! Ideally we want to get here < 500ms after the user clicks the app // Done! Ideally we want to get here < 500ms after the user clicks the app
sound.play('STARTUP')
console.timeEnd('init') console.timeEnd('init')
} }
function delayedInit () { function delayedInit () {
lazyLoadCast() lazyLoadCast()
sound.preload() sound.preload()
telemetry.init(state)
} }
// Lazily loads Chromecast and Airplay support // Lazily loads Chromecast and Airplay support
@@ -167,7 +175,6 @@ function dispatch (action, ...args) {
} }
if (action === 'openTorrentAddress') { if (action === 'openTorrentAddress') {
state.modal = { id: 'open-torrent-address-modal' } state.modal = { id: 'open-torrent-address-modal' }
update()
} }
if (action === 'createTorrent') { if (action === 'createTorrent') {
createTorrent(args[0] /* options */) createTorrent(args[0] /* options */)
@@ -190,11 +197,14 @@ function dispatch (action, ...args) {
if (action === 'openTorrentContextMenu') { if (action === 'openTorrentContextMenu') {
openTorrentContextMenu(args[0] /* infoHash */) openTorrentContextMenu(args[0] /* infoHash */)
} }
if (action === 'openDevice') { if (action === 'toggleCastMenu') {
lazyLoadCast().open(args[0] /* deviceType */) lazyLoadCast().toggleMenu(args[0] /* deviceType */)
} }
if (action === 'closeDevice') { if (action === 'selectCastDevice') {
lazyLoadCast().close() lazyLoadCast().selectDevice(args[0] /* index */)
}
if (action === 'stopCasting') {
lazyLoadCast().stop()
} }
if (action === 'setDimensions') { if (action === 'setDimensions') {
setDimensions(args[0] /* dimensions */) setDimensions(args[0] /* dimensions */)
@@ -252,6 +262,7 @@ function dispatch (action, ...args) {
} }
if (action === 'mediaError') { if (action === 'mediaError') {
if (state.location.url() === 'player') { if (state.location.url() === 'player') {
state.playing.result = 'error'
state.playing.location = 'error' state.playing.location = 'error'
ipcRenderer.send('checkForVLC') ipcRenderer.send('checkForVLC')
ipcRenderer.once('checkForVLC', function (e, isInstalled) { ipcRenderer.once('checkForVLC', function (e, isInstalled) {
@@ -263,6 +274,9 @@ function dispatch (action, ...args) {
}) })
} }
} }
if (action === 'mediaSuccess') {
state.playing.result = 'success'
}
if (action === 'mediaTimeUpdate') { if (action === 'mediaTimeUpdate') {
state.playing.lastTimeUpdate = new Date().getTime() state.playing.lastTimeUpdate = new Date().getTime()
state.playing.isStalled = false state.playing.isStalled = false
@@ -319,6 +333,9 @@ function dispatch (action, ...args) {
if (action === 'setTitle') { if (action === 'setTitle') {
state.window.title = args[0] /* title */ state.window.title = args[0] /* title */
} }
if (action === 'uncaughtError') {
telemetry.logUncaughtError(args[0] /* process */, args[1] /* error */)
}
// Update the virtual-dom, unless it's just a mouse move event // Update the virtual-dom, unless it's just a mouse move event
if (action !== 'mediaMouseMoved' || showOrHidePlayerControls()) { if (action !== 'mediaMouseMoved' || showOrHidePlayerControls()) {
@@ -355,11 +372,18 @@ function pause () {
function playPause () { function playPause () {
if (state.location.url() !== 'player') return if (state.location.url() !== 'player') return
if (state.playing.isPaused) { if (state.playing.isPaused) {
play() play()
} else { } else {
pause() pause()
} }
// force rerendering if window is hidden,
// in order to bypass `raf` and play/pause media immediately
if (!state.window.isVisible) render(state)
ipcRenderer.send('updateThumbnailBar', state.playing.isPaused)
} }
function jumpToTime (time) { function jumpToTime (time) {
@@ -470,6 +494,8 @@ function setupIpc () {
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args)) ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args)) ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err))
ipcRenderer.send('ipcReady') ipcRenderer.send('ipcReady')
} }
@@ -544,7 +570,7 @@ function onOpen (files) {
function isTorrent (file) { function isTorrent (file) {
var name = typeof file === 'string' ? file : file.name var name = typeof file === 'string' ? file : file.name
var isTorrentFile = path.extname(name).toLowerCase() === '.torrent' var isTorrentFile = path.extname(name).toLowerCase() === '.torrent'
var isMagnet = typeof file === 'string' && /^magnet:/.test(file) var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
return isTorrentFile || isMagnet return isTorrentFile || isMagnet
} }
@@ -737,6 +763,7 @@ function findFilesRecursive (paths, cb) {
findFilesRecursive([path], function (fileObjs) { findFilesRecursive([path], function (fileObjs) {
ret = ret.concat(fileObjs) ret = ret.concat(fileObjs)
if (++numComplete === paths.length) { if (++numComplete === paths.length) {
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
cb(ret) cb(ret)
} }
}) })
@@ -973,10 +1000,13 @@ function openPlayer (infoHash, index, cb) {
// update UI to show pending playback // update UI to show pending playback
if (torrentSummary.progress !== 1) sound.play('PLAY') if (torrentSummary.progress !== 1) sound.play('PLAY')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'requested' torrentSummary.playStatus = 'requested'
update() update()
var timeout = setTimeout(function () { var timeout = setTimeout(function () {
telemetry.logPlayAttempt('timeout')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'timeout' /* no seeders available? */ torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR') sound.play('ERROR')
cb(new Error('Playback timed out. Try again.')) cb(new Error('Playback timed out. Try again.'))
@@ -1042,25 +1072,41 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
} }
function closePlayer (cb) { function closePlayer (cb) {
console.log('closePlayer')
// Quit any external players, like Chromecast/Airplay/etc or VLC
if (isCasting()) { if (isCasting()) {
Cast.close() Cast.stop()
} }
if (state.playing.location === 'vlc') { if (state.playing.location === 'vlc') {
ipcRenderer.send('vlcQuit') ipcRenderer.send('vlcQuit')
} }
state.window.title = config.APP_WINDOW_TITLE
// Lets save volume for later // Save volume (this session only, not in state.saved)
state.previousVolume = state.playing.volume state.previousVolume = state.playing.volume
// Telemetry: track what happens after the user clicks play
var result = state.playing.result // 'success' or 'error'
if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed
else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc
else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame
else console.error('Unknown state.playing.result', state.playing.result)
// Reset the window contents back to the home screen
state.window.title = config.APP_WINDOW_TITLE
state.playing = State.getDefaultPlayState() state.playing = State.getDefaultPlayState()
state.server = null state.server = null
// Reset the window size and location back to where it was
if (state.window.isFullScreen) { if (state.window.isFullScreen) {
dispatch('toggleFullScreen', false) dispatch('toggleFullScreen', false)
} }
restoreBounds() restoreBounds()
// Tell the WebTorrent process to kill the torrent-to-HTTP server
ipcRenderer.send('wt-stop-server') ipcRenderer.send('wt-stop-server')
// Tell the OS we're no longer playing media, laptops allowed to sleep again
ipcRenderer.send('unblockPowerSave') ipcRenderer.send('unblockPowerSave')
ipcRenderer.send('onPlayerClose') ipcRenderer.send('onPlayerClose')
@@ -1091,9 +1137,14 @@ function toggleTorrent (infoHash) {
} }
// TODO: use torrentKey, not infoHash // TODO: use torrentKey, not infoHash
function deleteTorrent (infoHash) { function deleteTorrent (infoHash, deleteData) {
ipcRenderer.send('wt-stop-torrenting', infoHash) ipcRenderer.send('wt-stop-torrenting', infoHash)
if (deleteData) {
var torrentSummary = getTorrentSummary(infoHash)
moveItemToTrash(torrentSummary)
}
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash) var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) state.saved.torrents.splice(index, 1) if (index > -1) state.saved.torrents.splice(index, 1)
saveStateThrottled() saveStateThrottled()
@@ -1119,6 +1170,20 @@ function openTorrentContextMenu (infoHash) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
var menu = new electron.remote.Menu() var menu = new electron.remote.Menu()
menu.append(new electron.remote.MenuItem({
label: 'Remove From List',
click: () => deleteTorrent(torrentSummary.infoHash, false)
}))
menu.append(new electron.remote.MenuItem({
label: 'Remove Data File',
click: () => deleteTorrent(torrentSummary.infoHash, true)
}))
menu.append(new electron.remote.MenuItem({
type: 'separator'
}))
if (torrentSummary.files) { if (torrentSummary.files) {
menu.append(new electron.remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder', label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
@@ -1159,6 +1224,10 @@ function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary)) ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
} }
function moveItemToTrash (torrentSummary) {
ipcRenderer.send('moveItemToTrash', getTorrentPath(torrentSummary))
}
function saveTorrentFileAs (torrentSummary) { function saveTorrentFileAs (torrentSummary) {
var newFileName = `${path.parse(torrentSummary.name).name}.torrent` var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
var opts = { var opts = {
@@ -1296,3 +1365,7 @@ function onBlur () {
state.window.isFocused = false state.window.isFocused = false
update() update()
} }
function onVisibilityChange () {
state.window.isVisible = !document.webkitHidden
}

View File

@@ -1,6 +1,6 @@
module.exports = OpenTorrentAddressModal module.exports = OpenTorrentAddressModal
var {dispatch} = require('../lib/dispatcher') var {dispatch, dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx') var hx = require('../lib/hx')
function OpenTorrentAddressModal (state) { function OpenTorrentAddressModal (state) {
@@ -11,8 +11,8 @@ function OpenTorrentAddressModal (state) {
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} /> <input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
</p> </p>
<p class='float-right'> <p class='float-right'>
<button class='button button-flat' onclick=${handleCancel}>CANCEL</button> <button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
<button class='button button-raised' onclick=${handleOK}>OK</button> <button class='button button-raised' onclick=${handleOK}>OK</button>
</p> </p>
<script>document.querySelector('#add-torrent-url').focus()</script> <script>document.querySelector('#add-torrent-url').focus()</script>
</div> </div>
@@ -27,7 +27,3 @@ function handleOK () {
dispatch('exitModal') dispatch('exitModal')
dispatch('addTorrent', document.querySelector('#add-torrent-url').value) dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
} }
function handleCancel () {
dispatch('exitModal')
}

View File

@@ -143,6 +143,7 @@ function renderMedia (state) {
} else if (elem.webkitAudioDecodedByteCount === 0) { } else if (elem.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported') dispatch('mediaError', 'Audio codec unsupported')
} else { } else {
dispatch('mediaSuccess')
elem.play() elem.play()
} }
} }
@@ -279,8 +280,10 @@ function renderCastScreen (state) {
} }
var isStarting = state.playing.location.endsWith('-pending') var isStarting = state.playing.location.endsWith('-pending')
var castName = state.playing.castName
var castStatus var castStatus
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected' if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
else castStatus = '' else castStatus = ''
// Show a nice title image, if possible // Show a nice title image, if possible
@@ -299,6 +302,30 @@ function renderCastScreen (state) {
` `
} }
function renderCastOptions (state) {
if (!state.devices.castMenu) return
var {location, devices} = state.devices.castMenu
var player = state.devices[location]
var items = devices.map(function (device, ix) {
var isSelected = player.device === device
var name = device.name
return hx`
<li onclick=${dispatcher('selectCastDevice', ix)}>
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
${name}
</li>
`
})
return hx`
<ul.options-list>
${items}
</ul>
`
}
function renderSubtitlesOptions (state) { function renderSubtitlesOptions (state) {
var subtitles = state.playing.subtitles var subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return if (!subtitles.tracks.length || !subtitles.showMenu) return
@@ -316,7 +343,7 @@ function renderSubtitlesOptions (state) {
var noneSelected = state.playing.subtitles.selectedIndex === -1 var noneSelected = state.playing.subtitles.selectedIndex === -1
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked') var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
return hx` return hx`
<ul.subtitles-list> <ul.options-list>
${items} ${items}
<li onclick=${dispatcher('selectSubtitle', -1)}> <li onclick=${dispatcher('selectSubtitle', -1)}>
<i.icon>${noneClass}</i> <i.icon>${noneClass}</i>
@@ -378,72 +405,49 @@ function renderPlayerControls (state) {
} }
// If we've detected a Chromecast or AppleTV, the user can play video there // If we've detected a Chromecast or AppleTV, the user can play video there
var isOnChromecast = state.playing.location.startsWith('chromecast') var castTypes = ['chromecast', 'airplay', 'dlna']
var isOnAirplay = state.playing.location.startsWith('airplay') var isCastingAnywhere = castTypes.some(
var isOnDlna = state.playing.location.startsWith('dlna') (castType) => state.playing.location.startsWith(castType))
var chromecastClass, chromecastHandler
var airplayClass, airplayHandler
var dlnaClass, dlnaHandler
if (isOnChromecast) {
chromecastClass = 'active'
dlnaClass = 'disabled'
airplayClass = 'disabled'
chromecastHandler = dispatcher('closeDevice')
airplayHandler = undefined
dlnaHandler = undefined
} else if (isOnAirplay) {
chromecastClass = 'disabled'
dlnaClass = 'disabled'
airplayClass = 'active'
chromecastHandler = undefined
airplayHandler = dispatcher('closeDevice')
dlnaHandler = undefined
} else if (isOnDlna) {
chromecastClass = 'disabled'
dlnaClass = 'active'
airplayClass = 'disabled'
chromecastHandler = undefined
airplayHandler = undefined
dlnaHandler = dispatcher('closeDevice')
} else {
chromecastClass = ''
airplayClass = ''
dlnaClass = ''
chromecastHandler = dispatcher('openDevice', 'chromecast')
airplayHandler = dispatcher('openDevice', 'airplay')
dlnaHandler = dispatcher('openDevice', 'dlna')
}
if (state.devices.chromecast || isOnChromecast) {
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
elements.push(hx`
<i.icon.device.float-right
class=${chromecastClass}
onclick=${chromecastHandler}>
${castIcon}
</i>
`)
}
if (state.devices.airplay || isOnAirplay) {
elements.push(hx`
<i.icon.device.float-right
class=${airplayClass}
onclick=${airplayHandler}>
airplay
</i>
`)
}
if (state.devices.dlna || isOnDlna) {
elements.push(hx`
<i
class='icon device float-right'
class=${dlnaClass}
onclick=${dlnaHandler}>
tv
</i>
`)
}
// render volume // Add the cast buttons. Icons for each cast type, connected/disconnected:
var buttonIcons = {
'chromecast': {true: 'cast_connected', false: 'cast'},
'airplay': {true: 'airplay', false: 'airplay'},
'dlna': {true: 'tv', false: 'tv'}
}
castTypes.forEach(function (castType) {
// Do we show this button (eg. the Chromecast button) at all?
var isCasting = state.playing.location.startsWith(castType)
var player = state.devices[castType]
if ((!player || player.getDevices().length === 0) && !isCasting) return
// Show the button. Three options for eg the Chromecast button:
var buttonClass, buttonHandler
if (isCasting) {
// Option 1: we are currently connected to Chromecast. Button stops the cast.
buttonClass = 'active'
buttonHandler = dispatcher('stopCasting')
} else if (isCastingAnywhere) {
// Option 2: we are currently connected somewhere else. Button disabled.
buttonClass = 'disabled'
buttonHandler = undefined
} else {
// Option 3: we are not connected anywhere. Button opens Chromecast menu.
buttonClass = ''
buttonHandler = dispatcher('toggleCastMenu', castType)
}
var buttonIcon = buttonIcons[castType][isCasting]
elements.push(hx`
<i.icon.device.float-right
class=${buttonClass}
onclick=${buttonHandler}>
${buttonIcon}
</i>
`)
})
// Render volume slider
var volume = state.playing.volume var volume = state.playing.volume
var volumeIcon = 'volume_' + ( var volumeIcon = 'volume_' + (
volume === 0 ? 'off' volume === 0 ? 'off'
@@ -496,6 +500,7 @@ function renderPlayerControls (state) {
return hx` return hx`
<div class='controls'> <div class='controls'>
${elements} ${elements}
${renderCastOptions(state)}
${renderSubtitlesOptions(state)} ${renderSubtitlesOptions(state)}
</div> </div>
` `

View File

@@ -10,9 +10,9 @@ function UpdateAvailableModal (state) {
<div class='update-available-modal'> <div class='update-available-modal'>
<p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p> <p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p>
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p> <p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
<p> <p class='float-right'>
<button class='primary' onclick=${handleOK}>Show Download Page</button> <button class='button button-flat' onclick=${handleCancel}>Skip This Release</button>
<button class='cancel' onclick=${handleCancel}>Skip This Release</button> <button class='button button-raised' onclick=${handleOK}>Show Download Page</button>
</p> </p>
</div> </div>
` `

View File

@@ -63,6 +63,10 @@ function init () {
ipc.send('ipcReadyWebTorrent') ipc.send('ipcReadyWebTorrent')
window.addEventListener('error', (e) =>
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
true)
setInterval(updateTorrentProgress, 1000) setInterval(updateTorrentProgress, 1000)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

View File

@@ -13,7 +13,7 @@ Exec=$EXEC_PATH %U
TryExec=$TRY_EXEC_PATH TryExec=$TRY_EXEC_PATH
StartupNotify=false StartupNotify=false
Categories=Network;FileTransfer;P2P; Categories=Network;FileTransfer;P2P;
MimeType=application/x-bittorrent;x-scheme-handler/magnet; MimeType=application/x-bittorrent;x-scheme-handler/magnet;x-scheme-handler/stream-magnet;
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress; Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;