Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f259b32cce | ||
|
|
eba9aa3e17 | ||
|
|
905eb1611e | ||
|
|
6d4b8c3c26 | ||
|
|
6a46609cca | ||
|
|
e872282221 | ||
|
|
24ac5af5b4 | ||
|
|
0ee92fb632 | ||
|
|
7cbc12c6ff | ||
|
|
60c82c73cd | ||
|
|
78790e73c7 | ||
|
|
bf464de16f | ||
|
|
0589963eed | ||
|
|
b79971eea5 | ||
|
|
d1e557f054 | ||
|
|
93ddb8d638 | ||
|
|
06fdd80845 | ||
|
|
b0b26f8300 | ||
|
|
1db890f5e7 | ||
|
|
0f80f96023 | ||
|
|
2d3673ea33 | ||
|
|
c28260611e | ||
|
|
b5dd00007a | ||
|
|
ac39264f3d | ||
|
|
667a04a41d | ||
|
|
51a9b2ea9b | ||
|
|
842ee5ca3c | ||
|
|
2cc67dbda7 | ||
|
|
70bc32614b | ||
|
|
9bf44d7d7e | ||
|
|
f48ecb87b2 | ||
|
|
1765aba681 | ||
|
|
c6063c759e | ||
|
|
bb4db2cede | ||
|
|
7c36898f78 | ||
|
|
23e8cdf216 | ||
|
|
5ffd4123a1 | ||
|
|
27e3c14f10 | ||
|
|
d57bfb825a | ||
|
|
0809e20a6e | ||
|
|
1ec305162e | ||
|
|
45d46d7ae8 | ||
|
|
adb41736d5 | ||
|
|
09d6fa550a | ||
|
|
75cc7383cb | ||
|
|
4d48b9e7c1 | ||
|
|
563e1ca0ba | ||
|
|
0fa3b678b0 | ||
|
|
8420c65d25 | ||
|
|
3232e96f6e | ||
|
|
110e25af73 | ||
|
|
8233faf518 | ||
|
|
39ae0343fc | ||
|
|
c637878603 | ||
|
|
91e61f6cd4 | ||
|
|
9f66418073 | ||
|
|
2c3d667675 | ||
|
|
6c68645b0f | ||
|
|
dc7ccb3956 | ||
|
|
a420936657 | ||
|
|
dcab7f72d4 | ||
|
|
a695f7c2d7 | ||
|
|
7677bff6d4 | ||
|
|
c7626997de | ||
|
|
91a1ab4a56 | ||
|
|
3e19cdfb0b | ||
|
|
2043dc2161 | ||
|
|
a9e36472c5 | ||
|
|
4df4f9b2ad | ||
|
|
4ad55173a5 | ||
|
|
b9c82dd6b2 | ||
|
|
8333f4893f | ||
|
|
f071965ae8 | ||
|
|
a4fa9ac666 | ||
|
|
939ee555b7 |
@@ -24,6 +24,7 @@
|
||||
- Thomas Watson Steen (w@tson.dk)
|
||||
- anonymlol (anonymlol7@gmail.com)
|
||||
- Gediminas Petrikas (gedas18@gmail.com)
|
||||
- Alberto Miranda (codealchemist@gmail.com)
|
||||
- Adam Gotlib (gotlib.adam+dev@gmail.com)
|
||||
- Rémi Jouannet (remijouannet@gmail.com)
|
||||
- Andrea Tupini (tupini07@gmail.com)
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
||||
# WebTorrent Desktop Version History
|
||||
|
||||
## v0.11.0 - 2016-08-19
|
||||
|
||||
### Added
|
||||
|
||||
- New Preference to "Set WebTorrent as default handler for torrents and magnet links" (#771)
|
||||
- New Preference to "Always play in VLC" (#674)
|
||||
- Check for missing default download path and torrent folders on start up (#776)
|
||||
|
||||
### Changed
|
||||
|
||||
- Do not automatically set WebTorrent as the default handler for torrents (#771)
|
||||
- Torrents can only be created from the home screen (#770)
|
||||
- Update Electron to 1.3.3 (#772)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow modifying the default tracker list on the Create Torrent page (#775)
|
||||
- Prevent opening multiple stacked Preference windows or Create Torrent windows (#770)
|
||||
- Windows: Player window auto-resize does not match video aspect ratio (#565)
|
||||
- Missing page title on Create Torrent page
|
||||
|
||||
## v0.10.0 - 2016-08-05
|
||||
|
||||
### Added
|
||||
|
||||
@@ -484,6 +484,11 @@ function buildLinux (cb) {
|
||||
dest: destPath,
|
||||
expand: true,
|
||||
cwd: filesPath
|
||||
}, {
|
||||
src: ['./**'],
|
||||
dest: path.join('/usr', 'share'),
|
||||
expand: true,
|
||||
cwd: path.join(config.STATIC_PATH, 'linux', 'share')
|
||||
}], function (err) {
|
||||
if (err) return cb(err)
|
||||
console.log(`Linux: Created ${destArch} deb.`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webtorrent-desktop",
|
||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||
"version": "0.10.0",
|
||||
"version": "0.12.0",
|
||||
"author": {
|
||||
"name": "WebTorrent, LLC",
|
||||
"email": "feross@webtorrent.io",
|
||||
@@ -22,7 +22,7 @@
|
||||
"deep-equal": "^1.0.1",
|
||||
"dlnacasts": "^0.1.0",
|
||||
"drag-drop": "^2.12.1",
|
||||
"electron-prebuilt": "1.3.2",
|
||||
"electron-prebuilt": "1.3.3",
|
||||
"fs-extra": "^0.30.0",
|
||||
"hat": "0.0.3",
|
||||
"iso-639-1": "^1.2.1",
|
||||
@@ -57,10 +57,10 @@
|
||||
"mkdirp": "^0.5.1",
|
||||
"nobin-debian-installer": "^0.0.10",
|
||||
"open": "0.0.5",
|
||||
"plist": "^1.2.0",
|
||||
"plist": "^2.0.1",
|
||||
"rimraf": "^2.5.2",
|
||||
"run-series": "^1.1.4",
|
||||
"standard": "^7.0.0"
|
||||
"standard": "^8.0.0-beta.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
|
||||
65
src/main/external-player.js
Normal file
65
src/main/external-player.js
Normal file
@@ -0,0 +1,65 @@
|
||||
module.exports = {
|
||||
spawn,
|
||||
kill,
|
||||
checkInstall
|
||||
}
|
||||
|
||||
var cp = require('child_process')
|
||||
var vlcCommand = require('vlc-command')
|
||||
|
||||
var log = require('./log')
|
||||
var windows = require('./windows')
|
||||
|
||||
// holds a ChildProcess while we're playing a video in an external player, null otherwise
|
||||
var proc
|
||||
|
||||
function checkInstall (path, cb) {
|
||||
// check for VLC if external player has not been specified by the user
|
||||
// otherwise assume the player is installed
|
||||
if (path == null) return vlcCommand((err) => cb(!err))
|
||||
process.nextTick(() => cb(true))
|
||||
}
|
||||
|
||||
function spawn (path, url, title) {
|
||||
if (path != null) return spawnExternal(path, [url])
|
||||
|
||||
// Try to find and use VLC if external player is not specified
|
||||
vlcCommand(function (err, vlcPath) {
|
||||
if (err) return windows.main.dispatch('externalPlayerNotFound')
|
||||
var args = ['--play-and-exit', '--video-on-top', '--quiet', `--meta-title=${JSON.stringify(title)}`, url]
|
||||
spawnExternal(vlcPath, args)
|
||||
})
|
||||
}
|
||||
|
||||
function kill () {
|
||||
if (!proc) return
|
||||
log('Killing external player, pid ' + proc.pid)
|
||||
proc.kill('SIGKILL') // kill -9
|
||||
proc = null
|
||||
}
|
||||
|
||||
function spawnExternal (path, args) {
|
||||
log('Running external media player:', path + ' ' + args.join(' '))
|
||||
|
||||
proc = cp.spawn(path, args, {stdio: 'ignore'})
|
||||
|
||||
// If it works, close the modal after a second
|
||||
var closeModalTimeout = setTimeout(() =>
|
||||
windows.main.dispatch('exitModal'), 1000)
|
||||
|
||||
proc.on('close', function (code) {
|
||||
clearTimeout(closeModalTimeout)
|
||||
if (!proc) return // Killed
|
||||
log('External player exited with code ', code)
|
||||
if (code === 0) {
|
||||
windows.main.dispatch('backToList')
|
||||
} else {
|
||||
windows.main.dispatch('externalPlayerNotFound')
|
||||
}
|
||||
proc = null
|
||||
})
|
||||
|
||||
proc.on('error', function (e) {
|
||||
log('External player error', e)
|
||||
})
|
||||
}
|
||||
@@ -280,6 +280,9 @@ function installLinux () {
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
|
||||
// Do not install in user dir if running on system
|
||||
if (/^\/opt/.test(process.execPath)) return
|
||||
|
||||
installDesktopFile()
|
||||
installIconFile()
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ var config = require('../config')
|
||||
var crashReporter = require('../crash-reporter')
|
||||
var dialog = require('./dialog')
|
||||
var dock = require('./dock')
|
||||
var handlers = require('./handlers')
|
||||
var ipc = require('./ipc')
|
||||
var log = require('./log')
|
||||
var menu = require('./menu')
|
||||
@@ -35,8 +34,8 @@ if (process.platform === 'win32') {
|
||||
}
|
||||
|
||||
if (!shouldQuit) {
|
||||
// Prevent multiple instances of app from running at same time. New instances signal
|
||||
// this instance and quit.
|
||||
// Prevent multiple instances of app from running at same time. New instances
|
||||
// signal this instance and quit.
|
||||
shouldQuit = app.makeSingleInstance(onAppOpen)
|
||||
if (shouldQuit) {
|
||||
app.quit()
|
||||
@@ -111,7 +110,6 @@ function init () {
|
||||
function delayedInit () {
|
||||
announcement.init()
|
||||
dock.init()
|
||||
handlers.install()
|
||||
tray.init()
|
||||
updater.init()
|
||||
userTasks.init()
|
||||
@@ -161,7 +159,10 @@ function processArgv (argv) {
|
||||
} else if (arg.startsWith('-psn')) {
|
||||
// Ignore Mac launchd "process serial number" argument
|
||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
||||
} else {
|
||||
} else if (arg !== '.') {
|
||||
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
|
||||
// development copy of WebTorrent is started while a production version is
|
||||
// running.
|
||||
torrentIds.push(arg)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,21 +8,19 @@ var app = electron.app
|
||||
|
||||
var dialog = require('./dialog')
|
||||
var dock = require('./dock')
|
||||
var handlers = require('./handlers')
|
||||
var log = require('./log')
|
||||
var menu = require('./menu')
|
||||
var powerSaveBlocker = require('./power-save-blocker')
|
||||
var shell = require('./shell')
|
||||
var shortcuts = require('./shortcuts')
|
||||
var vlc = require('./vlc')
|
||||
var externalPlayer = require('./external-player')
|
||||
var windows = require('./windows')
|
||||
var thumbar = require('./thumbar')
|
||||
|
||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||
var messageQueueMainToWebTorrent = []
|
||||
|
||||
// holds a ChildProcess while we're playing a video in VLC, null otherwise
|
||||
var vlcProcess
|
||||
|
||||
function init () {
|
||||
var ipc = electron.ipcMain
|
||||
|
||||
@@ -60,14 +58,14 @@ function init () {
|
||||
*/
|
||||
|
||||
ipc.on('onPlayerOpen', function () {
|
||||
menu.onPlayerOpen()
|
||||
menu.setPlayerOpen(true)
|
||||
powerSaveBlocker.enable()
|
||||
shortcuts.enable()
|
||||
thumbar.enable()
|
||||
})
|
||||
|
||||
ipc.on('onPlayerClose', function () {
|
||||
menu.onPlayerClose()
|
||||
menu.setPlayerOpen(false)
|
||||
powerSaveBlocker.disable()
|
||||
shortcuts.disable()
|
||||
thumbar.disable()
|
||||
@@ -91,6 +89,14 @@ function init () {
|
||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
||||
|
||||
/**
|
||||
* File handlers
|
||||
*/
|
||||
ipc.on('setDefaultFileHandler', (e, flag) => {
|
||||
if (flag) handlers.install()
|
||||
else handlers.uninstall()
|
||||
})
|
||||
|
||||
/**
|
||||
* Windows: Main
|
||||
*/
|
||||
@@ -103,54 +109,20 @@ function init () {
|
||||
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
||||
ipc.on('show', () => main.show())
|
||||
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
||||
ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
|
||||
|
||||
/**
|
||||
* VLC
|
||||
* TODO: Move most of this code to vlc.js
|
||||
* External Media Player
|
||||
*/
|
||||
|
||||
ipc.on('checkForVLC', function (e) {
|
||||
vlc.checkForVLC(function (isInstalled) {
|
||||
windows.main.send('checkForVLC', isInstalled)
|
||||
ipc.on('checkForExternalPlayer', function (e, path) {
|
||||
externalPlayer.checkInstall(path, function (isInstalled) {
|
||||
windows.main.send('checkForExternalPlayer', isInstalled)
|
||||
})
|
||||
})
|
||||
|
||||
ipc.on('vlcPlay', function (e, url, title) {
|
||||
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', `--meta-title=${title}`, url]
|
||||
log('Running vlc ' + args.join(' '))
|
||||
|
||||
vlc.spawn(args, function (err, proc) {
|
||||
if (err) return windows.main.dispatch('vlcNotFound')
|
||||
vlcProcess = proc
|
||||
|
||||
// If it works, close the modal after a second
|
||||
var closeModalTimeout = setTimeout(() =>
|
||||
windows.main.dispatch('exitModal'), 1000)
|
||||
|
||||
vlcProcess.on('close', function (code) {
|
||||
clearTimeout(closeModalTimeout)
|
||||
if (!vlcProcess) return // Killed
|
||||
log('VLC exited with code ', code)
|
||||
if (code === 0) {
|
||||
windows.main.dispatch('backToList')
|
||||
} else {
|
||||
windows.main.dispatch('vlcNotFound')
|
||||
}
|
||||
vlcProcess = null
|
||||
})
|
||||
|
||||
vlcProcess.on('error', function (e) {
|
||||
log('VLC error', e)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ipc.on('vlcQuit', function () {
|
||||
if (!vlcProcess) return
|
||||
log('Killing VLC, pid ' + vlcProcess.pid)
|
||||
vlcProcess.kill('SIGKILL') // kill -9
|
||||
vlcProcess = null
|
||||
})
|
||||
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
|
||||
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
||||
|
||||
// Capture all events
|
||||
var oldEmit = ipc.emit
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
module.exports = {
|
||||
init,
|
||||
onPlayerClose,
|
||||
onPlayerOpen,
|
||||
setPlayerOpen,
|
||||
setWindowFocus,
|
||||
setAllowNav,
|
||||
onToggleAlwaysOnTop,
|
||||
onToggleFullScreen,
|
||||
onWindowBlur,
|
||||
onWindowFocus
|
||||
onToggleFullScreen
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
@@ -24,26 +23,28 @@ function init () {
|
||||
electron.Menu.setApplicationMenu(menu)
|
||||
}
|
||||
|
||||
function onPlayerClose () {
|
||||
getMenuItem('Play/Pause').enabled = false
|
||||
getMenuItem('Increase Volume').enabled = false
|
||||
getMenuItem('Decrease Volume').enabled = false
|
||||
getMenuItem('Step Forward').enabled = false
|
||||
getMenuItem('Step Backward').enabled = false
|
||||
getMenuItem('Increase Speed').enabled = false
|
||||
getMenuItem('Decrease Speed').enabled = false
|
||||
getMenuItem('Add Subtitles File...').enabled = false
|
||||
function setPlayerOpen (flag) {
|
||||
getMenuItem('Play/Pause').enabled = flag
|
||||
getMenuItem('Increase Volume').enabled = flag
|
||||
getMenuItem('Decrease Volume').enabled = flag
|
||||
getMenuItem('Step Forward').enabled = flag
|
||||
getMenuItem('Step Backward').enabled = flag
|
||||
getMenuItem('Increase Speed').enabled = flag
|
||||
getMenuItem('Decrease Speed').enabled = flag
|
||||
getMenuItem('Add Subtitles File...').enabled = flag
|
||||
}
|
||||
|
||||
function onPlayerOpen () {
|
||||
getMenuItem('Play/Pause').enabled = true
|
||||
getMenuItem('Increase Volume').enabled = true
|
||||
getMenuItem('Decrease Volume').enabled = true
|
||||
getMenuItem('Step Forward').enabled = true
|
||||
getMenuItem('Step Backward').enabled = true
|
||||
getMenuItem('Increase Speed').enabled = true
|
||||
getMenuItem('Decrease Speed').enabled = true
|
||||
getMenuItem('Add Subtitles File...').enabled = true
|
||||
function setWindowFocus (flag) {
|
||||
getMenuItem('Full Screen').enabled = flag
|
||||
getMenuItem('Float on Top').enabled = flag
|
||||
}
|
||||
|
||||
// Disallow opening more screens on top of the current one.
|
||||
function setAllowNav (flag) {
|
||||
getMenuItem('Preferences').enabled = flag
|
||||
getMenuItem('Create New Torrent...').enabled = flag
|
||||
var item = getMenuItem('Create New Torrent from File...')
|
||||
if (item) item.enabled = flag
|
||||
}
|
||||
|
||||
function onToggleAlwaysOnTop (flag) {
|
||||
@@ -54,16 +55,6 @@ function onToggleFullScreen (flag) {
|
||||
getMenuItem('Full Screen').checked = flag
|
||||
}
|
||||
|
||||
function onWindowBlur () {
|
||||
getMenuItem('Full Screen').enabled = false
|
||||
getMenuItem('Float on Top').enabled = false
|
||||
}
|
||||
|
||||
function onWindowFocus () {
|
||||
getMenuItem('Full Screen').enabled = true
|
||||
getMenuItem('Float on Top').enabled = true
|
||||
}
|
||||
|
||||
function getMenuItem (label) {
|
||||
for (var i = 0; i < menu.items.length; i++) {
|
||||
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||
@@ -130,14 +121,6 @@ function getMenuTemplate () {
|
||||
},
|
||||
{
|
||||
role: 'selectall'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
click: () => windows.main.dispatch('preferences')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -350,6 +333,17 @@ function getMenuTemplate () {
|
||||
click: () => dialog.openSeedFile()
|
||||
})
|
||||
|
||||
// Edit menu (Windows, Linux)
|
||||
template[1].submenu.push(
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
click: () => windows.main.dispatch('preferences')
|
||||
})
|
||||
|
||||
// Help menu (Windows, Linux)
|
||||
template[4].submenu.push(
|
||||
{
|
||||
|
||||
@@ -13,7 +13,10 @@ var blockId = 0
|
||||
* display.
|
||||
*/
|
||||
function enable () {
|
||||
disable() // Stop the previous power saver block, if one exists.
|
||||
if (electron.powerSaveBlocker.isStarted(blockId)) {
|
||||
// If a power saver block already exists, do nothing.
|
||||
return
|
||||
}
|
||||
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||
log(`powerSaveBlocker.enable: ${blockId}`)
|
||||
}
|
||||
@@ -23,6 +26,7 @@ function enable () {
|
||||
*/
|
||||
function disable () {
|
||||
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
||||
// If a power saver block does not exist, do nothing.
|
||||
return
|
||||
}
|
||||
electron.powerSaveBlocker.stop(blockId)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
module.exports = {
|
||||
hasTray,
|
||||
init,
|
||||
onWindowBlur,
|
||||
onWindowFocus
|
||||
setWindowFocus
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
@@ -31,12 +30,7 @@ function hasTray () {
|
||||
return !!tray
|
||||
}
|
||||
|
||||
function onWindowBlur () {
|
||||
if (!tray) return
|
||||
updateTrayMenu()
|
||||
}
|
||||
|
||||
function onWindowFocus () {
|
||||
function setWindowFocus (flag) {
|
||||
if (!tray) return
|
||||
updateTrayMenu()
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
module.exports = {
|
||||
checkForVLC,
|
||||
spawn
|
||||
}
|
||||
|
||||
var cp = require('child_process')
|
||||
var vlcCommand = require('vlc-command')
|
||||
|
||||
// Finds if VLC is installed on Mac, Windows, or Linux.
|
||||
// Calls back with true or false: whether VLC was detected
|
||||
function checkForVLC (cb) {
|
||||
vlcCommand((err) => cb(!err))
|
||||
}
|
||||
|
||||
// Spawns VLC with child_process.spawn() to return a ChildProcess object
|
||||
// Calls back with (err, childProcess)
|
||||
function spawn (args, cb) {
|
||||
vlcCommand(function (err, vlcPath) {
|
||||
if (err) return cb(err)
|
||||
cb(null, cp.spawn(vlcPath, args))
|
||||
})
|
||||
}
|
||||
@@ -141,7 +141,13 @@ function setBounds (bounds, maximize) {
|
||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||
}
|
||||
main.win.setBounds(bounds, true)
|
||||
// Resize the window's content area (so window border doesn't need to be taken
|
||||
// into account)
|
||||
if (bounds.contentBounds) {
|
||||
main.win.setContentBounds(bounds, true)
|
||||
} else {
|
||||
main.win.setBounds(bounds, true)
|
||||
}
|
||||
} else {
|
||||
log('setBounds: not setting bounds because of window maximization')
|
||||
}
|
||||
@@ -204,13 +210,13 @@ function toggleFullScreen (flag) {
|
||||
}
|
||||
|
||||
function onWindowBlur () {
|
||||
menu.onWindowBlur()
|
||||
tray.onWindowBlur()
|
||||
menu.setWindowFocus(false)
|
||||
tray.setWindowFocus(false)
|
||||
}
|
||||
|
||||
function onWindowFocus () {
|
||||
menu.onWindowFocus()
|
||||
tray.onWindowFocus()
|
||||
menu.setWindowFocus(true)
|
||||
tray.setWindowFocus(true)
|
||||
}
|
||||
|
||||
function getIconPath () {
|
||||
|
||||
@@ -22,12 +22,12 @@ module.exports = class MediaController {
|
||||
if (state.location.url() === 'player') {
|
||||
state.playing.result = 'error'
|
||||
state.playing.location = 'error'
|
||||
ipcRenderer.send('checkForVLC')
|
||||
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
||||
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
|
||||
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
|
||||
state.modal = {
|
||||
id: 'unsupported-media-modal',
|
||||
error: error,
|
||||
vlcInstalled: isInstalled
|
||||
externalPlayerInstalled: isInstalled
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -42,15 +42,16 @@ module.exports = class MediaController {
|
||||
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||
}
|
||||
|
||||
vlcPlay () {
|
||||
ipcRenderer.send('vlcPlay', this.state.server.localURL, this.state.window.title)
|
||||
this.state.playing.location = 'vlc'
|
||||
openExternalPlayer () {
|
||||
var state = this.state
|
||||
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title)
|
||||
state.playing.location = 'external'
|
||||
}
|
||||
|
||||
vlcNotFound () {
|
||||
externalPlayerNotFound () {
|
||||
var modal = this.state.modal
|
||||
if (modal && modal.id === 'unsupported-media-modal') {
|
||||
modal.vlcNotFound = true
|
||||
modal.externalPlayerNotFound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const State = require('../lib/state')
|
||||
const ipcRenderer = electron.ipcRenderer
|
||||
|
||||
// Controls playback of torrents and files within torrents
|
||||
// both local (<video>,<audio>,VLC) and remote (cast)
|
||||
// both local (<video>,<audio>,external player) and remote (cast)
|
||||
module.exports = class PlaybackController {
|
||||
constructor (state, config, update) {
|
||||
this.state = state
|
||||
@@ -93,6 +93,10 @@ module.exports = class PlaybackController {
|
||||
|
||||
// Skip (aka seek) to a specific point, in seconds
|
||||
skipTo (time) {
|
||||
if (!Number.isFinite(time)) {
|
||||
console.error('Tried to skip to a non-finite time ' + time)
|
||||
return console.trace()
|
||||
}
|
||||
if (isCasting(this.state)) Cast.seek(time)
|
||||
else this.state.playing.jumpToTime = time
|
||||
}
|
||||
@@ -188,7 +192,7 @@ module.exports = class PlaybackController {
|
||||
}, 10000) /* give it a few seconds */
|
||||
|
||||
if (torrentSummary.status === 'paused') {
|
||||
dispatch('startTorrentingSummary', torrentSummary)
|
||||
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
||||
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
||||
} else {
|
||||
@@ -241,12 +245,20 @@ module.exports = class PlaybackController {
|
||||
return this.update()
|
||||
}
|
||||
|
||||
state.window.title = torrentSummary.files[state.playing.fileIndex].name
|
||||
|
||||
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
||||
if (this.state.saved.prefs.openExternalPlayer) {
|
||||
dispatch('openExternalPlayer')
|
||||
this.update()
|
||||
cb()
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, play the video
|
||||
dispatch('setTitle', torrentSummary.files[state.playing.fileIndex].name)
|
||||
this.update()
|
||||
|
||||
ipcRenderer.send('onPlayerOpen')
|
||||
|
||||
cb()
|
||||
})
|
||||
}
|
||||
@@ -259,8 +271,8 @@ module.exports = class PlaybackController {
|
||||
if (isCasting(state)) {
|
||||
Cast.stop()
|
||||
}
|
||||
if (state.playing.location === 'vlc') {
|
||||
ipcRenderer.send('vlcQuit')
|
||||
if (state.playing.location === 'external') {
|
||||
ipcRenderer.send('quitExternalPlayer')
|
||||
}
|
||||
|
||||
// Save volume (this session only, not in state.saved)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const State = require('../lib/state')
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const ipcRenderer = require('electron').ipcRenderer
|
||||
|
||||
// Controls the Preferences screen
|
||||
module.exports = class PrefsController {
|
||||
@@ -15,11 +16,15 @@ module.exports = class PrefsController {
|
||||
url: 'preferences',
|
||||
setup: function (cb) {
|
||||
// initialize preferences
|
||||
dispatch('setTitle', 'Preferences')
|
||||
state.window.title = 'Preferences'
|
||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||
ipcRenderer.send('setAllowNav', false)
|
||||
cb()
|
||||
},
|
||||
destroy: () => this.save()
|
||||
destroy: () => {
|
||||
ipcRenderer.send('setAllowNav', true)
|
||||
this.save()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -41,7 +46,11 @@ module.exports = class PrefsController {
|
||||
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||
save () {
|
||||
var state = this.state
|
||||
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
|
||||
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
|
||||
}
|
||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||
State.save(state)
|
||||
dispatch('checkDownloadPath')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const parallel = require('run-parallel')
|
||||
|
||||
const remote = electron.remote
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class SubtitlesController {
|
||||
@@ -11,7 +13,7 @@ module.exports = class SubtitlesController {
|
||||
}
|
||||
|
||||
openSubtitles () {
|
||||
electron.remote.dialog.showOpenDialog({
|
||||
remote.dialog.showOpenDialog({
|
||||
title: 'Select a subtitles file.',
|
||||
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
||||
properties: [ 'openFile' ]
|
||||
|
||||
@@ -24,8 +24,8 @@ module.exports = class TorrentListController {
|
||||
// Use path string instead of W3C File object
|
||||
torrentId = torrentId.path
|
||||
}
|
||||
|
||||
// Allow a instant.io link to be pasted
|
||||
// TODO: remove this once support is added to webtorrent core
|
||||
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||
}
|
||||
@@ -40,12 +40,21 @@ module.exports = class TorrentListController {
|
||||
|
||||
// Shows the Create Torrent page with options to seed a given file or folder
|
||||
showCreateTorrent (files) {
|
||||
// You can only create torrents from the home screen.
|
||||
if (this.state.location.url() !== 'home') {
|
||||
return dispatch('error', 'Please go back to the torrent list before creating a new torrent.')
|
||||
}
|
||||
|
||||
// Files will either be an array of file objects, which we can send directly
|
||||
// to the create-torrent screen
|
||||
if (files.length === 0 || typeof files[0] !== 'string') {
|
||||
this.state.location.go({
|
||||
url: 'create-torrent',
|
||||
files: files
|
||||
files: files,
|
||||
setup: (cb) => {
|
||||
this.state.window.title = 'Create New Torrent'
|
||||
cb(null)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -67,27 +76,38 @@ module.exports = class TorrentListController {
|
||||
var state = this.state
|
||||
var torrentKey = state.nextTorrentKey++
|
||||
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||
state.location.backToFirst(function () {
|
||||
state.location.clearForward('create-torrent')
|
||||
})
|
||||
state.location.cancel()
|
||||
}
|
||||
|
||||
// Starts downloading and/or seeding a given torrentSummary.
|
||||
startTorrentingSummary (torrentSummary) {
|
||||
var s = torrentSummary
|
||||
startTorrentingSummary (torrentKey) {
|
||||
var s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||
if (!s) throw new Error('Missing key: ' + torrentKey)
|
||||
|
||||
// Backward compatibility for config files save before we had torrentKey
|
||||
if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++
|
||||
// New torrent: give it a path
|
||||
if (!s.path) {
|
||||
// Use Downloads folder by default
|
||||
s.path = this.state.saved.prefs.downloadPath
|
||||
return start()
|
||||
}
|
||||
|
||||
// Use Downloads folder by default
|
||||
if (!s.path) s.path = this.state.saved.prefs.downloadPath
|
||||
// Existing torrent: check that the path is still there
|
||||
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
|
||||
if (err) {
|
||||
s.error = 'path-missing'
|
||||
return
|
||||
}
|
||||
start()
|
||||
})
|
||||
|
||||
ipcRenderer.send('wt-start-torrenting',
|
||||
s.torrentKey,
|
||||
TorrentSummary.getTorrentID(s),
|
||||
s.path,
|
||||
s.fileModtimes,
|
||||
s.selections)
|
||||
function start () {
|
||||
ipcRenderer.send('wt-start-torrenting',
|
||||
s.torrentKey,
|
||||
TorrentSummary.getTorrentID(s),
|
||||
s.path,
|
||||
s.fileModtimes,
|
||||
s.selections)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use torrentKey, not infoHash
|
||||
@@ -95,7 +115,7 @@ module.exports = class TorrentListController {
|
||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
if (torrentSummary.status === 'paused') {
|
||||
torrentSummary.status = 'new'
|
||||
this.startTorrentingSummary(torrentSummary)
|
||||
this.startTorrentingSummary(torrentSummary.torrentKey)
|
||||
sound.play('ENABLE')
|
||||
} else {
|
||||
torrentSummary.status = 'paused'
|
||||
@@ -252,7 +272,7 @@ function deleteFile (path) {
|
||||
// Delete all files in a torrent
|
||||
function moveItemToTrash (torrentSummary) {
|
||||
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
ipcRenderer.send('moveItemToTrash', filePath)
|
||||
if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
|
||||
}
|
||||
|
||||
function showItemInFolder (torrentSummary) {
|
||||
@@ -271,6 +291,7 @@ function saveTorrentFileAs (torrentSummary) {
|
||||
]
|
||||
}
|
||||
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
||||
if (!savePath) return // They clicked Cancel
|
||||
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||
if (err) return dispatch('error', err)
|
||||
|
||||
@@ -396,7 +396,9 @@ function stop () {
|
||||
|
||||
function stoppedCasting () {
|
||||
state.playing.location = 'local'
|
||||
state.playing.jumpToTime = state.playing.currentTime
|
||||
state.playing.jumpToTime = Number.isFinite(state.playing.currentTime)
|
||||
? state.playing.currentTime
|
||||
: 0
|
||||
update()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ module.exports = {
|
||||
run
|
||||
}
|
||||
|
||||
var semver = require('semver')
|
||||
var config = require('../../config')
|
||||
const semver = require('semver')
|
||||
const config = require('../../config')
|
||||
const TorrentSummary = require('./torrent-summary')
|
||||
const fs = require('fs')
|
||||
|
||||
// Change `state.saved` (which will be saved back to config.json on exit) as
|
||||
// needed, for example to deal with config.json format changes across versions
|
||||
@@ -25,13 +27,19 @@ function run (state) {
|
||||
migrate_0_7_2(state.saved)
|
||||
}
|
||||
|
||||
if (semver.lt(version, '0.11.0')) {
|
||||
migrate_0_11_0(state.saved)
|
||||
}
|
||||
|
||||
if (semver.lt(version, '0.12.0')) {
|
||||
migrate_0_12_0(state.saved)
|
||||
}
|
||||
|
||||
// Config is now on the new version
|
||||
state.saved.version = config.APP_VERSION
|
||||
}
|
||||
|
||||
function migrate_0_7_0 (saved) {
|
||||
console.log('migrate to 0.7.0')
|
||||
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
|
||||
@@ -46,7 +54,6 @@ function migrate_0_7_0 (saved) {
|
||||
// * Finally, now we're getting rid of torrentPath altogether
|
||||
var src, dst
|
||||
if (ts.torrentPath) {
|
||||
console.log('replacing torrentPath %s', ts.torrentPath)
|
||||
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||
src = ts.torrentPath
|
||||
} else {
|
||||
@@ -63,7 +70,6 @@ function migrate_0_7_0 (saved) {
|
||||
|
||||
// Replace posterURL with posterFileName
|
||||
if (ts.posterURL) {
|
||||
console.log('replacing posterURL %s', ts.posterURL)
|
||||
var extension = path.extname(ts.posterURL)
|
||||
src = path.isAbsolute(ts.posterURL)
|
||||
? ts.posterURL
|
||||
@@ -87,9 +93,45 @@ function migrate_0_7_0 (saved) {
|
||||
}
|
||||
|
||||
function migrate_0_7_2 (saved) {
|
||||
if (!saved.prefs) {
|
||||
if (saved.prefs == null) {
|
||||
saved.prefs = {
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migrate_0_11_0 (saved) {
|
||||
if (saved.prefs.isFileHandler == null) {
|
||||
// The app used to make itself the default torrent file handler automatically
|
||||
saved.prefs.isFileHandler = true
|
||||
}
|
||||
}
|
||||
|
||||
function migrate_0_12_0 (saved) {
|
||||
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
|
||||
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
|
||||
}
|
||||
delete saved.prefs.playInVlc
|
||||
|
||||
// Undo a terrible bug where clicking Play on a default torrent on a fresh
|
||||
// install results in a "path missing" error
|
||||
// See https://github.com/feross/webtorrent-desktop/pull/806
|
||||
var defaultTorrentFiles = [
|
||||
'6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent',
|
||||
'88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent',
|
||||
'6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent',
|
||||
'02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent',
|
||||
'3ba219a8634bf7bae3d848192b2da75ae995589d.torrent'
|
||||
]
|
||||
saved.torrents.forEach(function (torrentSummary) {
|
||||
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
|
||||
var fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
if (!fileOrFolder) return
|
||||
try {
|
||||
fs.statSync(fileOrFolder)
|
||||
} catch (e) {
|
||||
// Default torrent with "missing path" error. Clear path.
|
||||
delete torrentSummary.path
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -100,7 +100,10 @@ function setupSavedState (cb) {
|
||||
|
||||
var saved = {
|
||||
prefs: {
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
||||
isFileHandler: false,
|
||||
openExternalPlayer: false,
|
||||
externalPlayerPath: null
|
||||
},
|
||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||
@@ -200,6 +203,9 @@ function save (state, cb) {
|
||||
if (key === 'playStatus') {
|
||||
continue // Don't save whether a torrent is playing / pending
|
||||
}
|
||||
if (key === 'error') {
|
||||
continue // Don't save error states
|
||||
}
|
||||
torrent[key] = x[key]
|
||||
}
|
||||
return torrent
|
||||
|
||||
@@ -24,6 +24,7 @@ function init (state) {
|
||||
}
|
||||
|
||||
var now = new Date()
|
||||
telemetry.version = config.APP_VERSION
|
||||
telemetry.timestamp = now.toISOString()
|
||||
telemetry.localTime = now.toTimeString()
|
||||
telemetry.screens = getScreenInfo()
|
||||
@@ -32,6 +33,8 @@ function init (state) {
|
||||
|
||||
if (config.IS_PRODUCTION) {
|
||||
postToServer()
|
||||
// If the user keeps WebTorrent running for a long time, post every 24h
|
||||
setInterval(postToServer, 12 * 3600 * 1000)
|
||||
} else {
|
||||
// Development: telemetry used only for local debugging
|
||||
// Empty uncaught errors, etc at the start of every run
|
||||
@@ -116,8 +119,6 @@ function getApproxNumTorrents (state) {
|
||||
|
||||
// An uncaught error happened in the main process or in one of the windows
|
||||
function logUncaughtError (procName, err) {
|
||||
console.error('uncaught error', procName, err)
|
||||
|
||||
// Not initialized yet? Ignore.
|
||||
// Hopefully uncaught errors immediately on startup are fixed in dev
|
||||
if (!telemetry) return
|
||||
@@ -125,7 +126,10 @@ function logUncaughtError (procName, err) {
|
||||
var message, stack
|
||||
if (err instanceof Error) {
|
||||
message = err.message
|
||||
stack = err.stack
|
||||
// Remove the first part of each file path in the stack trace.
|
||||
// - Privacy: remove personal info like C:\Users\<full name>
|
||||
// - Aggregation: this lets us find which stacktraces occur often
|
||||
stack = err.stack.replace(/\(.*app.asar/g, '(...')
|
||||
} else {
|
||||
message = String(err)
|
||||
stack = ''
|
||||
|
||||
@@ -52,5 +52,6 @@ function getByKey (state, torrentKey) {
|
||||
// module. Store root folder explicitly to avoid hacky path processing below.
|
||||
function getFileOrFolder (torrentSummary) {
|
||||
var ts = torrentSummary
|
||||
if (!ts.path || !ts.files || ts.files.length === 0) return null
|
||||
return path.join(ts.path, ts.files[0].path.split('/')[0])
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ const dragDrop = require('drag-drop')
|
||||
const electron = require('electron')
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const fs = require('fs')
|
||||
|
||||
const config = require('../config')
|
||||
const App = require('./views/app')
|
||||
@@ -77,17 +78,20 @@ function onState (err, _state) {
|
||||
// Restart everything we were torrenting last time the app ran
|
||||
resumeTorrents()
|
||||
|
||||
// Calling update() updates the UI given the current state
|
||||
// Do this at least once a second to give every file in every torrentSummary
|
||||
// a progress bar and to keep the cursor in sync when playing a video
|
||||
setInterval(update, 1000)
|
||||
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
|
||||
|
||||
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
||||
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
||||
|
||||
// Listen for messages from the main process
|
||||
setupIpc()
|
||||
|
||||
// Calling update() updates the UI given the current state
|
||||
// Do this at least once a second to give every file in every torrentSummary
|
||||
// a progress bar and to keep the cursor in sync when playing a video
|
||||
setInterval(update, 1000)
|
||||
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
|
||||
// Warn if the download dir is gone, eg b/c an external drive is unplugged
|
||||
checkDownloadPath()
|
||||
|
||||
// OS integrations:
|
||||
// ...drag and drop files/text to start torrenting or seeding
|
||||
@@ -175,8 +179,7 @@ const dispatchHandlers = {
|
||||
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
||||
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
||||
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
||||
'startTorrentingSummary': (torrentSummary) =>
|
||||
controllers.torrentList.startTorrentingSummary(torrentSummary),
|
||||
'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
|
||||
|
||||
// Playback
|
||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
||||
@@ -195,14 +198,14 @@ const dispatchHandlers = {
|
||||
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),
|
||||
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect),
|
||||
|
||||
// Local media: <video>, <audio>, VLC
|
||||
// Local media: <video>, <audio>, external players
|
||||
'mediaStalled': () => controllers.media.mediaStalled(),
|
||||
'mediaError': (err) => controllers.media.mediaError(err),
|
||||
'mediaSuccess': () => controllers.media.mediaSuccess(),
|
||||
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
|
||||
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
|
||||
'vlcPlay': () => controllers.media.vlcPlay(),
|
||||
'vlcNotFound': () => controllers.media.vlcNotFound(),
|
||||
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
|
||||
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
|
||||
|
||||
// Remote casting: Chromecast, Airplay, etc
|
||||
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
|
||||
@@ -212,6 +215,7 @@ const dispatchHandlers = {
|
||||
// Preferences screen
|
||||
'preferences': () => controllers.prefs.show(),
|
||||
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
|
||||
'checkDownloadPath': checkDownloadPath,
|
||||
|
||||
// Update (check for new versions on Linux, where there's no auto updater)
|
||||
'updateAvailable': (version) => controllers.update.updateAvailable(version),
|
||||
@@ -223,6 +227,7 @@ const dispatchHandlers = {
|
||||
'escapeBack': escapeBack,
|
||||
'back': () => state.location.back(),
|
||||
'forward': () => state.location.forward(),
|
||||
'cancel': () => state.location.cancel(),
|
||||
|
||||
// Controlling the window
|
||||
'setDimensions': setDimensions,
|
||||
@@ -312,8 +317,14 @@ function escapeBack () {
|
||||
// Starts all torrents that aren't paused on program startup
|
||||
function resumeTorrents () {
|
||||
state.saved.torrents
|
||||
.filter((torrentSummary) => torrentSummary.status !== 'paused')
|
||||
.forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary))
|
||||
.map((torrentSummary) => {
|
||||
// Torrent keys are ephemeral, reassigned each time the app runs.
|
||||
// On startup, give all torrents a key, even the ones that are paused.
|
||||
torrentSummary.torrentKey = state.nextTorrentKey++
|
||||
return torrentSummary
|
||||
})
|
||||
.filter((s) => s.status !== 'paused')
|
||||
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey))
|
||||
}
|
||||
|
||||
// Set window dimensions to match video dimensions or fill the screen
|
||||
@@ -351,7 +362,7 @@ function setDimensions (dimensions) {
|
||||
)
|
||||
|
||||
ipcRenderer.send('setAspectRatio', aspectRatio)
|
||||
ipcRenderer.send('setBounds', {x: null, y: null, width, height})
|
||||
ipcRenderer.send('setBounds', {contentBounds: true, x: null, y: null, width, height})
|
||||
state.playing.aspectRatio = aspectRatio
|
||||
}
|
||||
|
||||
@@ -360,25 +371,25 @@ function setDimensions (dimensions) {
|
||||
function onOpen (files) {
|
||||
if (!Array.isArray(files)) files = [ files ]
|
||||
|
||||
if (state.modal) {
|
||||
var url = state.location.url()
|
||||
var allTorrents = files.every(TorrentPlayer.isTorrent)
|
||||
var allSubtitles = files.every(controllers.subtitles.isSubtitle)
|
||||
|
||||
if (allTorrents) {
|
||||
// Drop torrents onto the app: go to home screen, add torrents, no matter what
|
||||
dispatch('backToList')
|
||||
// All .torrent files? Add them.
|
||||
files.forEach((file) => controllers.torrentList.addTorrent(file))
|
||||
} else if (url === 'player' && allSubtitles) {
|
||||
// Drop subtitles onto a playing video: add subtitles
|
||||
controllers.subtitles.addSubtitles(files, true)
|
||||
} else if (url === 'home') {
|
||||
// Drop files onto home screen: show Create Torrent
|
||||
state.modal = null
|
||||
}
|
||||
|
||||
var subtitles = files.filter(controllers.subtitles.isSubtitle)
|
||||
|
||||
if (state.location.url() === 'home' || subtitles.length === 0) {
|
||||
if (files.every(TorrentPlayer.isTorrent)) {
|
||||
if (state.location.url() !== 'home') {
|
||||
dispatch('backToList')
|
||||
}
|
||||
// All .torrent files? Add them.
|
||||
files.forEach((file) => controllers.torrentList.addTorrent(file))
|
||||
} else {
|
||||
// Show the Create Torrent screen. Let's seed those files.
|
||||
controllers.torrentList.showCreateTorrent(files)
|
||||
}
|
||||
} else if (state.location.url() === 'player') {
|
||||
controllers.subtitles.addSubtitles(subtitles, true)
|
||||
controllers.torrentList.showCreateTorrent(files)
|
||||
} else {
|
||||
// Drop files onto any other screen: show error
|
||||
return onError('Please go back to the torrent list before creating a new torrent.')
|
||||
}
|
||||
|
||||
update()
|
||||
@@ -432,3 +443,14 @@ function onFullscreenChanged (e, isFullScreen) {
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
function checkDownloadPath () {
|
||||
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
|
||||
if (err) {
|
||||
state.downloadPathStatus = 'missing'
|
||||
return console.error(err)
|
||||
}
|
||||
if (stat.isDirectory()) state.downloadPathStatus = 'ok'
|
||||
else state.downloadPathStatus = 'missing'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ module.exports = class App extends React.Component {
|
||||
var ModalContents = Modals[state.modal.id]
|
||||
return (
|
||||
<div key='modal' className='modal'>
|
||||
<div key='modal-background' className='modal-background'></div>
|
||||
<div key='modal-background' className='modal-background' />
|
||||
<div key='modal-content' className='modal-content'>
|
||||
<ModalContents state={state} />
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ module.exports = class CreateTorrentErrorPage extends React.Component {
|
||||
</p>
|
||||
</p>
|
||||
<p className='float-right'>
|
||||
<button className='button-flat light' onClick={dispatcher('back')}>
|
||||
<button className='button-flat light' onClick={dispatcher('cancel')}>
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@@ -73,11 +73,11 @@ module.exports = class CreateTorrentPage extends React.Component {
|
||||
<div key='advanced' className={'create-torrent-advanced ' + collapsedClass}>
|
||||
<div key='comment' className='torrent-attribute'>
|
||||
<label>Comment:</label>
|
||||
<textarea className='torrent-attribute torrent-comment'></textarea>
|
||||
<textarea className='torrent-attribute torrent-comment' />
|
||||
</div>
|
||||
<div key='trackers' className='torrent-attribute'>
|
||||
<label>Trackers:</label>
|
||||
<textarea className='torrent-attribute torrent-trackers' value={trackers}></textarea>
|
||||
<textarea className='torrent-attribute torrent-trackers' defaultValue={trackers} />
|
||||
</div>
|
||||
<div key='private' className='torrent-attribute'>
|
||||
<label>Private:</label>
|
||||
@@ -89,7 +89,7 @@ module.exports = class CreateTorrentPage extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div key='buttons' className='float-right'>
|
||||
<button key='cancel' className='button-flat light' onClick={dispatcher('back')}>Cancel</button>
|
||||
<button key='cancel' className='button-flat light' onClick={dispatcher('cancel')}>Cancel</button>
|
||||
<button key='create' className='button-raised' onClick={handleOK}>Create Torrent</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ const React = require('react')
|
||||
const Bitfield = require('bitfield')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
const zeroFill = require('zero-fill')
|
||||
const path = require('path')
|
||||
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
@@ -232,7 +233,7 @@ function renderAudioMetadata (state) {
|
||||
}
|
||||
|
||||
// Align the title with the other info, if available. Otherwise, center title
|
||||
var emptyLabel = (<label></label>)
|
||||
var emptyLabel = (<label />)
|
||||
elems.unshift((
|
||||
<div key='title' className='audio-title'>
|
||||
{elems.length ? emptyLabel : undefined}{title}
|
||||
@@ -281,9 +282,12 @@ function renderCastScreen (state) {
|
||||
castIcon = 'tv'
|
||||
castType = 'DLNA'
|
||||
isCast = true
|
||||
} else if (state.playing.location === 'vlc') {
|
||||
} else if (state.playing.location === 'external') {
|
||||
// TODO: get the player name in a more reliable way
|
||||
var playerPath = state.saved.prefs.externalPlayerPath
|
||||
var playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
|
||||
castIcon = 'tv'
|
||||
castType = 'VLC'
|
||||
castType = playerName
|
||||
isCast = false
|
||||
} else if (state.playing.location === 'error') {
|
||||
castIcon = 'error_outline'
|
||||
@@ -380,16 +384,16 @@ function renderPlayerControls (state) {
|
||||
<div
|
||||
key='cursor'
|
||||
className='playback-cursor'
|
||||
style={playbackCursorStyle}>
|
||||
</div>
|
||||
style={playbackCursorStyle}
|
||||
/>
|
||||
<div
|
||||
key='scrub-bar'
|
||||
className='scrub-bar'
|
||||
draggable='true'
|
||||
onDragStart={handleDragStart}
|
||||
onClick={handleScrub}
|
||||
onDrag={handleScrub}>
|
||||
</div>
|
||||
onDrag={handleScrub}
|
||||
/>
|
||||
</div>,
|
||||
|
||||
<i
|
||||
@@ -593,7 +597,7 @@ function renderLoadingBar (state) {
|
||||
width: (100 * part.count / fileProg.numPieces) + '%'
|
||||
}
|
||||
|
||||
return (<div key={i} className='loading-bar-part' style={style}></div>)
|
||||
return (<div key={i} className='loading-bar-part' style={style} />)
|
||||
})
|
||||
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const React = require('react')
|
||||
const remote = require('electron').remote
|
||||
const dialog = remote.dialog
|
||||
const path = require('path')
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
@@ -10,6 +11,7 @@ module.exports = class Preferences extends React.Component {
|
||||
return (
|
||||
<div className='preferences'>
|
||||
{renderGeneralSection(state)}
|
||||
{renderPlaybackSection(state)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -22,11 +24,23 @@ function renderGeneralSection (state) {
|
||||
description: '',
|
||||
icon: 'settings'
|
||||
}, [
|
||||
renderDownloadDirSelector(state)
|
||||
renderDownloadPathSelector(state),
|
||||
renderFileHandlers(state)
|
||||
])
|
||||
}
|
||||
|
||||
function renderDownloadDirSelector (state) {
|
||||
function renderPlaybackSection (state) {
|
||||
return renderSection({
|
||||
title: 'Playback',
|
||||
description: '',
|
||||
icon: 'settings'
|
||||
}, [
|
||||
renderOpenExternalPlayerSelector(state),
|
||||
renderExternalPlayerSelector(state)
|
||||
])
|
||||
}
|
||||
|
||||
function renderDownloadPathSelector (state) {
|
||||
return renderFileSelector({
|
||||
key: 'download-path',
|
||||
label: 'Download Path',
|
||||
@@ -39,7 +53,65 @@ function renderDownloadDirSelector (state) {
|
||||
},
|
||||
state.unsaved.prefs.downloadPath,
|
||||
function (filePath) {
|
||||
setStateValue('downloadPath', filePath)
|
||||
dispatch('updatePreferences', 'downloadPath', filePath)
|
||||
})
|
||||
}
|
||||
|
||||
function renderFileHandlers (state) {
|
||||
var definition = {
|
||||
key: 'file-handlers',
|
||||
label: 'Handle Torrent Files'
|
||||
}
|
||||
var buttonText = state.unsaved.prefs.isFileHandler
|
||||
? 'Remove default app for torrent files'
|
||||
: 'Make WebTorrent the default app for torrent files'
|
||||
var controls = [(
|
||||
<button key='toggle-handlers'
|
||||
className='btn'
|
||||
onClick={toggleFileHandlers}>
|
||||
{buttonText}
|
||||
</button>
|
||||
)]
|
||||
return renderControlGroup(definition, controls)
|
||||
|
||||
function toggleFileHandlers () {
|
||||
var isFileHandler = state.unsaved.prefs.isFileHandler
|
||||
dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
|
||||
}
|
||||
}
|
||||
|
||||
function renderExternalPlayerSelector (state) {
|
||||
return renderFileSelector({
|
||||
label: 'External Media Player',
|
||||
description: 'Progam that will be used to play media externally',
|
||||
property: 'externalPlayerPath',
|
||||
options: {
|
||||
title: 'Select media player executable',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
},
|
||||
state.unsaved.prefs.externalPlayerPath || '<VLC>',
|
||||
function (filePath) {
|
||||
if (path.extname(filePath) === '.app') {
|
||||
// Get executable in packaged mac app
|
||||
var name = path.basename(filePath, '.app')
|
||||
filePath += '/Contents/MacOS/' + name
|
||||
}
|
||||
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
||||
})
|
||||
}
|
||||
|
||||
function renderOpenExternalPlayerSelector (state) {
|
||||
return renderCheckbox({
|
||||
key: 'open-external-player',
|
||||
label: 'Play in External Player',
|
||||
description: 'Media will play in external player',
|
||||
property: 'openExternalPlayer',
|
||||
value: state.saved.prefs.openExternalPlayer
|
||||
},
|
||||
state.unsaved.prefs.openExternalPlayer,
|
||||
function (value) {
|
||||
dispatch('updatePreferences', 'openExternalPlayer', value)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,31 +139,59 @@ function renderSection (definition, controls) {
|
||||
)
|
||||
}
|
||||
|
||||
function renderCheckbox (definition, value, callback) {
|
||||
var iconClass = 'icon clickable'
|
||||
if (value) iconClass += ' enabled'
|
||||
|
||||
return (
|
||||
<div key='{definition.key}' className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='preference-title'>{definition.label}</div>
|
||||
</label>
|
||||
<div className='controls'>
|
||||
<label className='clickable' onClick={handleClick}>
|
||||
<i
|
||||
className={iconClass}
|
||||
id='{definition.property}'
|
||||
>
|
||||
check_circle
|
||||
</i>
|
||||
<span className='checkbox-label'>{definition.description}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
function handleClick () {
|
||||
callback(!value)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a file chooser
|
||||
// - defition should be {label, description, options}
|
||||
// options are passed to dialog.showOpenDialog
|
||||
// - value should be the current pref, a file or folder path
|
||||
// - callback takes a new file or folder path
|
||||
function renderFileSelector (definition, value, callback) {
|
||||
return (
|
||||
<div key={definition.key} className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='preference-title'>{definition.label}</div>
|
||||
<div className='preference-description'>{definition.description}</div>
|
||||
</label>
|
||||
<div className='controls'>
|
||||
<input type='text' className='file-picker-text'
|
||||
id={definition.property}
|
||||
disabled='disabled'
|
||||
value={value} />
|
||||
<button className='btn' onClick={handleClick}>
|
||||
<i className='icon'>folder_open</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
var controls = [(
|
||||
<input
|
||||
type='text'
|
||||
className='file-picker-text'
|
||||
key={definition.property}
|
||||
id={definition.property}
|
||||
disabled='disabled'
|
||||
value={value} />
|
||||
), (
|
||||
<button
|
||||
key={definition.property + '-btn'}
|
||||
className='btn'
|
||||
onClick={handleClick}>
|
||||
<i className='icon'>folder_open</i>
|
||||
</button>
|
||||
)]
|
||||
return renderControlGroup(definition, controls)
|
||||
|
||||
function handleClick () {
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
||||
if (!Array.isArray(filenames)) return
|
||||
@@ -100,6 +200,18 @@ function renderFileSelector (definition, value, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function setStateValue (property, value) {
|
||||
dispatch('updatePreferences', property, value)
|
||||
function renderControlGroup (definition, controls) {
|
||||
return (
|
||||
<div key={definition.key} className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='preference-title'>{definition.label}</div>
|
||||
<div className='preference-description'>{definition.description}</div>
|
||||
</label>
|
||||
<div className='controls'>
|
||||
{controls}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,16 +8,32 @@ const {dispatcher} = require('../lib/dispatcher')
|
||||
module.exports = class TorrentList extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
var torrentRows = state.saved.torrents.map(
|
||||
|
||||
var contents = []
|
||||
if (state.downloadPathStatus === 'missing') {
|
||||
contents.push(
|
||||
<div key='torrent-missing-path'>
|
||||
<p>Download path missing: {state.saved.prefs.downloadPath}</p>
|
||||
<p>Check that all drives are connected?</p>
|
||||
<p>Alternatively, choose a new download path
|
||||
in <a href='#' onClick={dispatcher('preferences')}>Preferences</a>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
var torrentElems = state.saved.torrents.map(
|
||||
(torrentSummary) => this.renderTorrent(torrentSummary)
|
||||
)
|
||||
contents.push(...torrentElems)
|
||||
contents.push(
|
||||
<div key='torrent-placeholder' className='torrent-placeholder'>
|
||||
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div key='torrent-list' className='torrent-list'>
|
||||
{torrentRows}
|
||||
<div key='torrent-placeholder' className='torrent-placeholder'>
|
||||
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
|
||||
</div>
|
||||
{contents}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -44,6 +60,7 @@ module.exports = class TorrentList extends React.Component {
|
||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
||||
if (isSelected) classes.push('selected')
|
||||
if (!infoHash) classes.push('disabled')
|
||||
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
||||
return (
|
||||
<div
|
||||
key={torrentSummary.torrentKey}
|
||||
@@ -67,8 +84,14 @@ module.exports = class TorrentList extends React.Component {
|
||||
|
||||
// If it's downloading/seeding then show progress info
|
||||
var prog = torrentSummary.progress
|
||||
if (torrentSummary.status !== 'paused' && prog) {
|
||||
elements.push((
|
||||
if (torrentSummary.error) {
|
||||
elements.push(
|
||||
<div key='progress-info' className='ellipsis'>
|
||||
{getErrorMessage(torrentSummary)}
|
||||
</div>
|
||||
)
|
||||
} else if (torrentSummary.status !== 'paused' && prog) {
|
||||
elements.push(
|
||||
<div key='progress-info' className='ellipsis'>
|
||||
{renderPercentProgress()}
|
||||
{renderTotalProgress()}
|
||||
@@ -77,7 +100,7 @@ module.exports = class TorrentList extends React.Component {
|
||||
{renderUploadSpeed()}
|
||||
{renderEta()}
|
||||
</div>
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
return (<div key='metadata' className='metadata'>{elements}</div>)
|
||||
@@ -174,8 +197,9 @@ module.exports = class TorrentList extends React.Component {
|
||||
}
|
||||
|
||||
// Only show the play button for torrents that contain playable media
|
||||
var playButton
|
||||
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||
var playButton, downloadButton
|
||||
var noErrors = !torrentSummary.error
|
||||
if (noErrors && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||
playButton = (
|
||||
<i
|
||||
key='play-button'
|
||||
@@ -186,11 +210,8 @@ module.exports = class TorrentList extends React.Component {
|
||||
</i>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key='buttons' className='buttons'>
|
||||
{positionElem}
|
||||
{playButton}
|
||||
if (noErrors) {
|
||||
downloadButton = (
|
||||
<i
|
||||
key='download-button'
|
||||
className={'button-round icon download ' + torrentSummary.status}
|
||||
@@ -198,6 +219,14 @@ module.exports = class TorrentList extends React.Component {
|
||||
onClick={dispatcher('toggleTorrent', infoHash)}>
|
||||
{downloadIcon}
|
||||
</i>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key='buttons' className='buttons'>
|
||||
{positionElem}
|
||||
{playButton}
|
||||
{downloadButton}
|
||||
<i
|
||||
key='delete-button'
|
||||
className='icon delete'
|
||||
@@ -212,12 +241,26 @@ module.exports = class TorrentList extends React.Component {
|
||||
// Show files, per-file download status and play buttons, and so on
|
||||
renderTorrentDetails (torrentSummary) {
|
||||
var filesElement
|
||||
if (!torrentSummary.files) {
|
||||
// We don't know what files this torrent contains
|
||||
var message = torrentSummary.status === 'paused'
|
||||
? 'Failed to load torrent info. Click the download button to try again...'
|
||||
: 'Downloading torrent info...'
|
||||
filesElement = (<div key='files' className='files warning'>{message}</div>)
|
||||
if (torrentSummary.error || !torrentSummary.files) {
|
||||
var message = ''
|
||||
if (torrentSummary.error === 'path-missing') {
|
||||
// Special case error: this torrent's download dir or file is missing
|
||||
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
} else if (torrentSummary.error) {
|
||||
// General error for this torrent: just show the message
|
||||
message = torrentSummary.error.message || torrentSummary.error
|
||||
} else if (torrentSummary.status === 'paused') {
|
||||
// No file info, no infohash, and we're not trying to download from the DHT
|
||||
message = 'Failed to load torrent info. Click the download button to try again...'
|
||||
} else {
|
||||
// No file info, no infohash, trying to load from the DHT
|
||||
message = 'Downloading torrent info...'
|
||||
}
|
||||
filesElement = (
|
||||
<div key='files' className='files warning'>
|
||||
{message}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
// We do know the files. List them and show download stats for each one
|
||||
var fileRows = torrentSummary.files
|
||||
@@ -316,15 +359,28 @@ module.exports = class TorrentList extends React.Component {
|
||||
<div key='radial-progress' className={'radial-progress ' + cssClass}>
|
||||
<div key='circle' className='circle'>
|
||||
<div key='mask-full' className='mask full' style={transformFill}>
|
||||
<div key='fill' className='fill' style={transformFill}></div>
|
||||
<div key='fill' className='fill' style={transformFill} />
|
||||
</div>
|
||||
<div key='mask-half' className='mask half'>
|
||||
<div key='fill' className='fill' style={transformFill}></div>
|
||||
<div key='fill-fix' className='fill fix' style={transformFix}></div>
|
||||
<div key='fill' className='fill' style={transformFill} />
|
||||
<div key='fill-fix' className='fill fix' style={transformFix} />
|
||||
</div>
|
||||
</div>
|
||||
<div key='inset' className='inset'></div>
|
||||
<div key='inset' className='inset' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorMessage (torrentSummary) {
|
||||
var err = torrentSummary.error
|
||||
if (err === 'path-missing') {
|
||||
return (
|
||||
<span>
|
||||
Path missing.<br />
|
||||
Fix and restart the app, or delete the torrent.
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return 'Error'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const React = require('react')
|
||||
const electron = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
const {dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
@@ -10,11 +11,15 @@ module.exports = class UnsupportedMediaModal extends React.Component {
|
||||
var message = (err && err.getMessage)
|
||||
? err.getMessage()
|
||||
: err
|
||||
var actionButton = state.modal.vlcInstalled
|
||||
? (<button className='button-raised' onClick={dispatcher('vlcPlay')}>Play in VLC</button>)
|
||||
var playerPath = state.saved.prefs.externalPlayerPath
|
||||
var playerName = playerPath
|
||||
? path.basename(playerPath).split('.')[0]
|
||||
: 'VLC'
|
||||
var actionButton = state.modal.externalPlayerInstalled
|
||||
? (<button className='button-raised' onClick={dispatcher('openExternalPlayer')}>Play in {playerName}</button>)
|
||||
: (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>)
|
||||
var vlcMessage = state.modal.vlcNotFound
|
||||
? 'Couldn\'t run VLC. Please make sure it\'s installed.'
|
||||
var playerMessage = state.modal.externalPlayerNotFound
|
||||
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
||||
: ''
|
||||
return (
|
||||
<div>
|
||||
@@ -24,7 +29,7 @@ module.exports = class UnsupportedMediaModal extends React.Component {
|
||||
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
|
||||
{actionButton}
|
||||
</p>
|
||||
<p className='error-text'>{vlcMessage}</p>
|
||||
<p className='error-text'>{playerMessage}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -34,6 +39,6 @@ module.exports = class UnsupportedMediaModal extends React.Component {
|
||||
|
||||
// TODO: dcposch send a dispatch rather than modifying state directly
|
||||
var state = this.props.state
|
||||
state.modal.vlcInstalled = true // Assume they'll install it successfully
|
||||
state.modal.externalPlayerInstalled = true // Assume they'll install it successfully
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,18 @@ module.exports = class UpdateAvailableModal extends React.Component {
|
||||
<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 className='float-right'>
|
||||
<button className='button button-flat' onClick={handleCancel}>Skip This Release</button>
|
||||
<button className='button button-raised' onClick={handleOK}>Show Download Page</button>
|
||||
<button className='button button-flat' onClick={handleSkip}>Skip This Release</button>
|
||||
<button className='button button-raised' onClick={handleShow}>Show Download Page</button>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
function handleOK () {
|
||||
function handleShow () {
|
||||
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
|
||||
dispatch('exitModal')
|
||||
}
|
||||
|
||||
function handleCancel () {
|
||||
function handleSkip () {
|
||||
dispatch('skipVersion', state.modal.version)
|
||||
dispatch('exitModal')
|
||||
}
|
||||
|
||||
33
static/linux/share/applications/webtorrent-desktop.desktop
Normal file
33
static/linux/share/applications/webtorrent-desktop.desktop
Normal file
@@ -0,0 +1,33 @@
|
||||
[Desktop Entry]
|
||||
Name=WebTorrent
|
||||
Version=1.0
|
||||
GenericName=BitTorrent Client
|
||||
X-GNOME-FullName=WebTorrent
|
||||
Comment=Download and share files over BitTorrent
|
||||
Encoding=UTF-8
|
||||
Type=Application
|
||||
Icon=webtorrent-desktop
|
||||
Terminal=false
|
||||
Path=/opt/webtorrent-desktop
|
||||
Exec=/opt/webtorrent-desktop/WebTorrent %U
|
||||
TryExec=/opt/webtorrent-desktop/WebTorrent
|
||||
StartupNotify=false
|
||||
Categories=Network;FileTransfer;P2P;
|
||||
MimeType=application/x-bittorrent;x-scheme-handler/magnet;x-scheme-handler/stream-magnet;
|
||||
|
||||
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
|
||||
|
||||
[Desktop Action CreateNewTorrent]
|
||||
Name=Create New Torrent...
|
||||
Exec=/opt/webtorrent-desktop/WebTorrent -n
|
||||
Path=/opt/webtorrent-desktop
|
||||
|
||||
[Desktop Action OpenTorrentFile]
|
||||
Name=Open Torrent File...
|
||||
Exec=/opt/webtorrent-desktop/WebTorrent -o
|
||||
Path=/opt/webtorrent-desktop
|
||||
|
||||
[Desktop Action OpenTorrentAddress]
|
||||
Name=Open Torrent Address...
|
||||
Exec=/opt/webtorrent-desktop/WebTorrent -u
|
||||
Path=/opt/webtorrent-desktop
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -122,6 +122,10 @@ table {
|
||||
* UTILITY CLASSES
|
||||
*/
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -551,6 +555,19 @@ input[type='text'] {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
* TORRENT LIST: ERRORS
|
||||
*/
|
||||
|
||||
.torrent-list p {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
.torrent-list a {
|
||||
color: #99f;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* TORRENT LIST: DRAG-DROP TARGET
|
||||
*/
|
||||
@@ -919,6 +936,10 @@ video::-webkit-media-text-track-container {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
.preferences .icon.enabled {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.preferences .btn {
|
||||
display: inline-block;
|
||||
-webkit-appearance: button;
|
||||
@@ -1066,6 +1087,14 @@ video::-webkit-media-text-track-container {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.preferences .checkbox {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* MEDIA OVERLAY / AUDIO DETAILS
|
||||
|
||||
Reference in New Issue
Block a user