Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e34223fc94 | ||
|
|
15f733f11c | ||
|
|
7526b18507 | ||
|
|
0af6007632 | ||
|
|
1bc3cd1d51 | ||
|
|
92bafd695d | ||
|
|
78a2ee4e85 | ||
|
|
8b9346d767 | ||
|
|
06d3bd3f93 | ||
|
|
1af7e4ef19 | ||
|
|
8e64e4120b | ||
|
|
b983559763 | ||
|
|
e62527de23 | ||
|
|
1f51f35f8e | ||
|
|
c3686417e3 | ||
|
|
746e10c025 | ||
|
|
98389fc07c | ||
|
|
aaebf93db4 | ||
|
|
12500dfb64 | ||
|
|
acc8e7923a | ||
|
|
9aa5775528 | ||
|
|
2a2d71289a | ||
|
|
ae28e34fd5 | ||
|
|
6b175e7d40 | ||
|
|
2c6d74e8ef | ||
|
|
3b832595fe | ||
|
|
bf372029fb | ||
|
|
17ce7e519c | ||
|
|
1f6a112df7 | ||
|
|
0ec6fb5a93 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,11 +1,19 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
|
## v0.7.2 - 2016-06-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix exception that affects users upgrading from v0.5.1 or older
|
||||||
|
- Ensure `state.saved.prefs` configuration exists
|
||||||
|
- Fix window title on "About WebTorrent" window
|
||||||
|
|
||||||
## v0.7.1 - 2016-06-02
|
## 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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
[](https://github.com/feross/standard)
|
[](https://github.com/feross/standard)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
35
main/thumbnail.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
module.exports = {
|
||||||
|
showPlayerThumbnailBar,
|
||||||
|
hidePlayerThumbnailBar,
|
||||||
|
updateThumbarButtons
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = require('path')
|
||||||
|
var config = require('../config')
|
||||||
|
|
||||||
|
var windows = require('./windows')
|
||||||
|
|
||||||
|
// gets called on player open
|
||||||
|
function showPlayerThumbnailBar () {
|
||||||
|
updateThumbarButtons(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets called on player close
|
||||||
|
function hidePlayerThumbnailBar () {
|
||||||
|
windows.main.win.setThumbarButtons([])
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateThumbarButtons (isPaused) {
|
||||||
|
var icon = isPaused ? 'PlayThumbnailBarButton.png' : 'PauseThumbnailBarButton.png'
|
||||||
|
var tooltip = isPaused ? 'Play' : 'Pause'
|
||||||
|
var buttons = [
|
||||||
|
{
|
||||||
|
tooltip: tooltip,
|
||||||
|
icon: path.join(config.STATIC_PATH, icon),
|
||||||
|
click: function () {
|
||||||
|
windows.main.send('dispatch', 'playPause')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
windows.main.win.setThumbarButtons(buttons)
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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.0",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -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
140
renderer/lib/telemetry.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
// Collects anonymous usage stats and uncaught errors
|
||||||
|
// Reports back so that we can improve WebTorrent Desktop
|
||||||
|
module.exports = {
|
||||||
|
init,
|
||||||
|
logUncaughtError,
|
||||||
|
logPlayAttempt
|
||||||
|
}
|
||||||
|
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const electron = require('electron')
|
||||||
|
const https = require('https')
|
||||||
|
const os = require('os')
|
||||||
|
const url = require('url')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
|
var telemetry
|
||||||
|
|
||||||
|
function init (state) {
|
||||||
|
telemetry = state.saved.telemetry
|
||||||
|
if (!telemetry) {
|
||||||
|
telemetry = state.saved.telemetry = createSummary()
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = new Date()
|
||||||
|
telemetry.timestamp = now.toISOString()
|
||||||
|
telemetry.localTime = now.toTimeString()
|
||||||
|
telemetry.screens = getScreenInfo()
|
||||||
|
telemetry.system = getSystemInfo()
|
||||||
|
telemetry.approxNumTorrents = getApproxNumTorrents(state)
|
||||||
|
|
||||||
|
postToServer(telemetry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset () {
|
||||||
|
telemetry.uncaughtErrors = []
|
||||||
|
telemetry.playAttempts = {
|
||||||
|
total: 0,
|
||||||
|
success: 0,
|
||||||
|
timeout: 0,
|
||||||
|
error: 0,
|
||||||
|
abandoned: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postToServer () {
|
||||||
|
// Serialize the telemetry summary
|
||||||
|
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
|
||||||
|
|
||||||
|
// POST to our server
|
||||||
|
var options = url.parse(config.TELEMETRY_URL)
|
||||||
|
options.method = 'POST'
|
||||||
|
options.headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': payload.length
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = https.request(options, function (res) {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
console.log('Successfully posted telemetry summary')
|
||||||
|
reset()
|
||||||
|
} else {
|
||||||
|
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
req.on('error', function (e) {
|
||||||
|
console.error('Couldn\'t post telemetry summary', e)
|
||||||
|
})
|
||||||
|
req.write(payload)
|
||||||
|
req.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new telemetry summary. Gives the user a unique ID,
|
||||||
|
// collects screen resolution, etc
|
||||||
|
function createSummary () {
|
||||||
|
// Make a 256-bit random unique ID
|
||||||
|
var userID = crypto.randomBytes(32).toString('hex')
|
||||||
|
return { userID }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track screen resolution
|
||||||
|
function getScreenInfo () {
|
||||||
|
return electron.screen.getAllDisplays().map((screen) => ({
|
||||||
|
width: screen.size.width,
|
||||||
|
height: screen.size.height,
|
||||||
|
scaleFactor: screen.scaleFactor
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track basic system info like OS version and amount of RAM
|
||||||
|
function getSystemInfo () {
|
||||||
|
return {
|
||||||
|
osPlatform: process.platform,
|
||||||
|
osRelease: os.type() + ' ' + os.release(),
|
||||||
|
architecture: os.arch(),
|
||||||
|
totalMemoryMB: os.totalmem() / (1 << 20),
|
||||||
|
numCores: os.cpus().length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the number of torrents, rounded to the nearest power of two
|
||||||
|
function getApproxNumTorrents (state) {
|
||||||
|
var exactNum = state.saved.torrents.length
|
||||||
|
if (exactNum === 0) return 0
|
||||||
|
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||||
|
var log2 = Math.log(exactNum) / Math.log(2)
|
||||||
|
return 1 << Math.round(log2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An uncaught error happened in the main process or in one of the windows
|
||||||
|
function logUncaughtError (procName, err) {
|
||||||
|
var message, stack
|
||||||
|
if (typeof err === 'string') {
|
||||||
|
message = err
|
||||||
|
stack = ''
|
||||||
|
} else {
|
||||||
|
message = err.message
|
||||||
|
stack = err.stack
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to POST the telemetry object, make sure it stays < 100kb
|
||||||
|
if (telemetry.uncaughtErrors.length > 20) return
|
||||||
|
if (message.length > 1000) message = message.substring(0, 1000)
|
||||||
|
if (stack.length > 1000) stack = stack.substring(0, 1000)
|
||||||
|
|
||||||
|
telemetry.uncaughtErrors.push({process: procName, message, stack})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user pressed play. It either worked, timed out, or showed the
|
||||||
|
// "Play in VLC" codec error
|
||||||
|
function logPlayAttempt (result) {
|
||||||
|
if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) {
|
||||||
|
return console.error('Unknown play attempt result', result)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attempts = telemetry.playAttempts
|
||||||
|
attempts.total = (attempts.total || 0) + 1
|
||||||
|
attempts[result] = (attempts[result] || 0) + 1
|
||||||
|
}
|
||||||
@@ -24,7 +24,8 @@ function isVideo (file) {
|
|||||||
'.mp4',
|
'.mp4',
|
||||||
'.mpg',
|
'.mpg',
|
||||||
'.ogv',
|
'.ogv',
|
||||||
'.webm'
|
'.webm',
|
||||||
|
'.wmv'
|
||||||
].includes(ext)
|
].includes(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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')
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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'},
|
||||||
|
'dnla': {true: 'tv', false: 'tv'}
|
||||||
|
}
|
||||||
|
castTypes.forEach(function (castType) {
|
||||||
|
// Do we show this button (eg. the Chromecast button) at all?
|
||||||
|
var isCasting = state.playing.location.startsWith(castType)
|
||||||
|
var player = state.devices[castType]
|
||||||
|
if ((!player || player.getDevices().length === 0) && !isCasting) return
|
||||||
|
|
||||||
|
// Show the button. Three options for eg the Chromecast button:
|
||||||
|
var buttonClass, buttonHandler
|
||||||
|
if (isCasting) {
|
||||||
|
// Option 1: we are currently connected to Chromecast. Button stops the cast.
|
||||||
|
buttonClass = 'active'
|
||||||
|
buttonHandler = dispatcher('stopCasting')
|
||||||
|
} else if (isCastingAnywhere) {
|
||||||
|
// Option 2: we are currently connected somewhere else. Button disabled.
|
||||||
|
buttonClass = 'disabled'
|
||||||
|
buttonHandler = undefined
|
||||||
|
} else {
|
||||||
|
// Option 3: we are not connected anywhere. Button opens Chromecast menu.
|
||||||
|
buttonClass = ''
|
||||||
|
buttonHandler = dispatcher('toggleCastMenu', castType)
|
||||||
|
}
|
||||||
|
var buttonIcon = buttonIcons[castType][isCasting]
|
||||||
|
|
||||||
|
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>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -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>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
static/PauseThumbnailBarButton.png
Normal file
BIN
static/PauseThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 B |
BIN
static/PlayThumbnailBarButton.png
Normal file
BIN
static/PlayThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 B |
Reference in New Issue
Block a user