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)
|
- Thomas Watson Steen (w@tson.dk)
|
||||||
- anonymlol (anonymlol7@gmail.com)
|
- anonymlol (anonymlol7@gmail.com)
|
||||||
- Gediminas Petrikas (gedas18@gmail.com)
|
- Gediminas Petrikas (gedas18@gmail.com)
|
||||||
|
- Alberto Miranda (codealchemist@gmail.com)
|
||||||
- Adam Gotlib (gotlib.adam+dev@gmail.com)
|
- Adam Gotlib (gotlib.adam+dev@gmail.com)
|
||||||
- Rémi Jouannet (remijouannet@gmail.com)
|
- Rémi Jouannet (remijouannet@gmail.com)
|
||||||
- Andrea Tupini (tupini07@gmail.com)
|
- Andrea Tupini (tupini07@gmail.com)
|
||||||
|
|||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
|||||||
# WebTorrent Desktop Version History
|
# 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
|
## v0.10.0 - 2016-08-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -484,6 +484,11 @@ function buildLinux (cb) {
|
|||||||
dest: destPath,
|
dest: destPath,
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: filesPath
|
cwd: filesPath
|
||||||
|
}, {
|
||||||
|
src: ['./**'],
|
||||||
|
dest: path.join('/usr', 'share'),
|
||||||
|
expand: true,
|
||||||
|
cwd: path.join(config.STATIC_PATH, 'linux', 'share')
|
||||||
}], function (err) {
|
}], function (err) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log(`Linux: Created ${destArch} deb.`)
|
console.log(`Linux: Created ${destArch} deb.`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||||
"version": "0.10.0",
|
"version": "0.12.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@webtorrent.io",
|
"email": "feross@webtorrent.io",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.1.0",
|
"dlnacasts": "^0.1.0",
|
||||||
"drag-drop": "^2.12.1",
|
"drag-drop": "^2.12.1",
|
||||||
"electron-prebuilt": "1.3.2",
|
"electron-prebuilt": "1.3.3",
|
||||||
"fs-extra": "^0.30.0",
|
"fs-extra": "^0.30.0",
|
||||||
"hat": "0.0.3",
|
"hat": "0.0.3",
|
||||||
"iso-639-1": "^1.2.1",
|
"iso-639-1": "^1.2.1",
|
||||||
@@ -57,10 +57,10 @@
|
|||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"nobin-debian-installer": "^0.0.10",
|
"nobin-debian-installer": "^0.0.10",
|
||||||
"open": "0.0.5",
|
"open": "0.0.5",
|
||||||
"plist": "^1.2.0",
|
"plist": "^2.0.1",
|
||||||
"rimraf": "^2.5.2",
|
"rimraf": "^2.5.2",
|
||||||
"run-series": "^1.1.4",
|
"run-series": "^1.1.4",
|
||||||
"standard": "^7.0.0"
|
"standard": "^8.0.0-beta.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0.0"
|
"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 config = require('../config')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
|
|
||||||
|
// Do not install in user dir if running on system
|
||||||
|
if (/^\/opt/.test(process.execPath)) return
|
||||||
|
|
||||||
installDesktopFile()
|
installDesktopFile()
|
||||||
installIconFile()
|
installIconFile()
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ var config = require('../config')
|
|||||||
var crashReporter = require('../crash-reporter')
|
var crashReporter = require('../crash-reporter')
|
||||||
var dialog = require('./dialog')
|
var dialog = require('./dialog')
|
||||||
var dock = require('./dock')
|
var dock = require('./dock')
|
||||||
var handlers = require('./handlers')
|
|
||||||
var ipc = require('./ipc')
|
var ipc = require('./ipc')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
@@ -35,8 +34,8 @@ if (process.platform === 'win32') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldQuit) {
|
if (!shouldQuit) {
|
||||||
// Prevent multiple instances of app from running at same time. New instances signal
|
// Prevent multiple instances of app from running at same time. New instances
|
||||||
// this instance and quit.
|
// signal this instance and quit.
|
||||||
shouldQuit = app.makeSingleInstance(onAppOpen)
|
shouldQuit = app.makeSingleInstance(onAppOpen)
|
||||||
if (shouldQuit) {
|
if (shouldQuit) {
|
||||||
app.quit()
|
app.quit()
|
||||||
@@ -111,7 +110,6 @@ function init () {
|
|||||||
function delayedInit () {
|
function delayedInit () {
|
||||||
announcement.init()
|
announcement.init()
|
||||||
dock.init()
|
dock.init()
|
||||||
handlers.install()
|
|
||||||
tray.init()
|
tray.init()
|
||||||
updater.init()
|
updater.init()
|
||||||
userTasks.init()
|
userTasks.init()
|
||||||
@@ -161,7 +159,10 @@ function processArgv (argv) {
|
|||||||
} else if (arg.startsWith('-psn')) {
|
} else if (arg.startsWith('-psn')) {
|
||||||
// Ignore Mac launchd "process serial number" argument
|
// Ignore Mac launchd "process serial number" argument
|
||||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
// 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)
|
torrentIds.push(arg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,21 +8,19 @@ var app = electron.app
|
|||||||
|
|
||||||
var dialog = require('./dialog')
|
var dialog = require('./dialog')
|
||||||
var dock = require('./dock')
|
var dock = require('./dock')
|
||||||
|
var handlers = require('./handlers')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var powerSaveBlocker = require('./power-save-blocker')
|
var powerSaveBlocker = require('./power-save-blocker')
|
||||||
var shell = require('./shell')
|
var shell = require('./shell')
|
||||||
var shortcuts = require('./shortcuts')
|
var shortcuts = require('./shortcuts')
|
||||||
var vlc = require('./vlc')
|
var externalPlayer = require('./external-player')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
var thumbar = require('./thumbar')
|
var thumbar = require('./thumbar')
|
||||||
|
|
||||||
// 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 = []
|
||||||
|
|
||||||
// holds a ChildProcess while we're playing a video in VLC, null otherwise
|
|
||||||
var vlcProcess
|
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
var ipc = electron.ipcMain
|
var ipc = electron.ipcMain
|
||||||
|
|
||||||
@@ -60,14 +58,14 @@ function init () {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('onPlayerOpen', function () {
|
ipc.on('onPlayerOpen', function () {
|
||||||
menu.onPlayerOpen()
|
menu.setPlayerOpen(true)
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
shortcuts.enable()
|
shortcuts.enable()
|
||||||
thumbar.enable()
|
thumbar.enable()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerClose', function () {
|
ipc.on('onPlayerClose', function () {
|
||||||
menu.onPlayerClose()
|
menu.setPlayerOpen(false)
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
shortcuts.disable()
|
shortcuts.disable()
|
||||||
thumbar.disable()
|
thumbar.disable()
|
||||||
@@ -91,6 +89,14 @@ function init () {
|
|||||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||||
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...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
|
* Windows: Main
|
||||||
*/
|
*/
|
||||||
@@ -103,54 +109,20 @@ function init () {
|
|||||||
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
||||||
ipc.on('show', () => main.show())
|
ipc.on('show', () => main.show())
|
||||||
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
||||||
|
ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VLC
|
* External Media Player
|
||||||
* TODO: Move most of this code to vlc.js
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('checkForVLC', function (e) {
|
ipc.on('checkForExternalPlayer', function (e, path) {
|
||||||
vlc.checkForVLC(function (isInstalled) {
|
externalPlayer.checkInstall(path, function (isInstalled) {
|
||||||
windows.main.send('checkForVLC', isInstalled)
|
windows.main.send('checkForExternalPlayer', isInstalled)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('vlcPlay', function (e, url, title) {
|
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
|
||||||
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', `--meta-title=${title}`, url]
|
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
// Capture all events
|
// Capture all events
|
||||||
var oldEmit = ipc.emit
|
var oldEmit = ipc.emit
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
onPlayerClose,
|
setPlayerOpen,
|
||||||
onPlayerOpen,
|
setWindowFocus,
|
||||||
|
setAllowNav,
|
||||||
onToggleAlwaysOnTop,
|
onToggleAlwaysOnTop,
|
||||||
onToggleFullScreen,
|
onToggleFullScreen
|
||||||
onWindowBlur,
|
|
||||||
onWindowFocus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -24,26 +23,28 @@ function init () {
|
|||||||
electron.Menu.setApplicationMenu(menu)
|
electron.Menu.setApplicationMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function setPlayerOpen (flag) {
|
||||||
getMenuItem('Play/Pause').enabled = false
|
getMenuItem('Play/Pause').enabled = flag
|
||||||
getMenuItem('Increase Volume').enabled = false
|
getMenuItem('Increase Volume').enabled = flag
|
||||||
getMenuItem('Decrease Volume').enabled = false
|
getMenuItem('Decrease Volume').enabled = flag
|
||||||
getMenuItem('Step Forward').enabled = false
|
getMenuItem('Step Forward').enabled = flag
|
||||||
getMenuItem('Step Backward').enabled = false
|
getMenuItem('Step Backward').enabled = flag
|
||||||
getMenuItem('Increase Speed').enabled = false
|
getMenuItem('Increase Speed').enabled = flag
|
||||||
getMenuItem('Decrease Speed').enabled = false
|
getMenuItem('Decrease Speed').enabled = flag
|
||||||
getMenuItem('Add Subtitles File...').enabled = false
|
getMenuItem('Add Subtitles File...').enabled = flag
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerOpen () {
|
function setWindowFocus (flag) {
|
||||||
getMenuItem('Play/Pause').enabled = true
|
getMenuItem('Full Screen').enabled = flag
|
||||||
getMenuItem('Increase Volume').enabled = true
|
getMenuItem('Float on Top').enabled = flag
|
||||||
getMenuItem('Decrease Volume').enabled = true
|
}
|
||||||
getMenuItem('Step Forward').enabled = true
|
|
||||||
getMenuItem('Step Backward').enabled = true
|
// Disallow opening more screens on top of the current one.
|
||||||
getMenuItem('Increase Speed').enabled = true
|
function setAllowNav (flag) {
|
||||||
getMenuItem('Decrease Speed').enabled = true
|
getMenuItem('Preferences').enabled = flag
|
||||||
getMenuItem('Add Subtitles File...').enabled = true
|
getMenuItem('Create New Torrent...').enabled = flag
|
||||||
|
var item = getMenuItem('Create New Torrent from File...')
|
||||||
|
if (item) item.enabled = flag
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleAlwaysOnTop (flag) {
|
function onToggleAlwaysOnTop (flag) {
|
||||||
@@ -54,16 +55,6 @@ function onToggleFullScreen (flag) {
|
|||||||
getMenuItem('Full Screen').checked = flag
|
getMenuItem('Full Screen').checked = flag
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
|
||||||
getMenuItem('Full Screen').enabled = false
|
|
||||||
getMenuItem('Float on Top').enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowFocus () {
|
|
||||||
getMenuItem('Full Screen').enabled = true
|
|
||||||
getMenuItem('Float on Top').enabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMenuItem (label) {
|
function getMenuItem (label) {
|
||||||
for (var i = 0; i < menu.items.length; i++) {
|
for (var i = 0; i < menu.items.length; i++) {
|
||||||
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||||
@@ -130,14 +121,6 @@ function getMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Preferences',
|
|
||||||
accelerator: 'CmdOrCtrl+,',
|
|
||||||
click: () => windows.main.dispatch('preferences')
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -350,6 +333,17 @@ function getMenuTemplate () {
|
|||||||
click: () => dialog.openSeedFile()
|
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)
|
// Help menu (Windows, Linux)
|
||||||
template[4].submenu.push(
|
template[4].submenu.push(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ var blockId = 0
|
|||||||
* display.
|
* display.
|
||||||
*/
|
*/
|
||||||
function enable () {
|
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')
|
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||||
log(`powerSaveBlocker.enable: ${blockId}`)
|
log(`powerSaveBlocker.enable: ${blockId}`)
|
||||||
}
|
}
|
||||||
@@ -23,6 +26,7 @@ function enable () {
|
|||||||
*/
|
*/
|
||||||
function disable () {
|
function disable () {
|
||||||
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
||||||
|
// If a power saver block does not exist, do nothing.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
electron.powerSaveBlocker.stop(blockId)
|
electron.powerSaveBlocker.stop(blockId)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
hasTray,
|
hasTray,
|
||||||
init,
|
init,
|
||||||
onWindowBlur,
|
setWindowFocus
|
||||||
onWindowFocus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -31,12 +30,7 @@ function hasTray () {
|
|||||||
return !!tray
|
return !!tray
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
function setWindowFocus (flag) {
|
||||||
if (!tray) return
|
|
||||||
updateTrayMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowFocus () {
|
|
||||||
if (!tray) return
|
if (!tray) return
|
||||||
updateTrayMenu()
|
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)
|
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
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 {
|
} else {
|
||||||
log('setBounds: not setting bounds because of window maximization')
|
log('setBounds: not setting bounds because of window maximization')
|
||||||
}
|
}
|
||||||
@@ -204,13 +210,13 @@ function toggleFullScreen (flag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
function onWindowBlur () {
|
||||||
menu.onWindowBlur()
|
menu.setWindowFocus(false)
|
||||||
tray.onWindowBlur()
|
tray.setWindowFocus(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowFocus () {
|
function onWindowFocus () {
|
||||||
menu.onWindowFocus()
|
menu.setWindowFocus(true)
|
||||||
tray.onWindowFocus()
|
tray.setWindowFocus(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconPath () {
|
function getIconPath () {
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ module.exports = class MediaController {
|
|||||||
if (state.location.url() === 'player') {
|
if (state.location.url() === 'player') {
|
||||||
state.playing.result = 'error'
|
state.playing.result = 'error'
|
||||||
state.playing.location = 'error'
|
state.playing.location = 'error'
|
||||||
ipcRenderer.send('checkForVLC')
|
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
|
||||||
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
|
||||||
state.modal = {
|
state.modal = {
|
||||||
id: 'unsupported-media-modal',
|
id: 'unsupported-media-modal',
|
||||||
error: error,
|
error: error,
|
||||||
vlcInstalled: isInstalled
|
externalPlayerInstalled: isInstalled
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -42,15 +42,16 @@ module.exports = class MediaController {
|
|||||||
this.state.playing.mouseStationarySince = new Date().getTime()
|
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
vlcPlay () {
|
openExternalPlayer () {
|
||||||
ipcRenderer.send('vlcPlay', this.state.server.localURL, this.state.window.title)
|
var state = this.state
|
||||||
this.state.playing.location = 'vlc'
|
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title)
|
||||||
|
state.playing.location = 'external'
|
||||||
}
|
}
|
||||||
|
|
||||||
vlcNotFound () {
|
externalPlayerNotFound () {
|
||||||
var modal = this.state.modal
|
var modal = this.state.modal
|
||||||
if (modal && modal.id === 'unsupported-media-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
|
const ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
// Controls playback of torrents and files within torrents
|
// 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 {
|
module.exports = class PlaybackController {
|
||||||
constructor (state, config, update) {
|
constructor (state, config, update) {
|
||||||
this.state = state
|
this.state = state
|
||||||
@@ -93,6 +93,10 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Skip (aka seek) to a specific point, in seconds
|
// Skip (aka seek) to a specific point, in seconds
|
||||||
skipTo (time) {
|
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)
|
if (isCasting(this.state)) Cast.seek(time)
|
||||||
else this.state.playing.jumpToTime = time
|
else this.state.playing.jumpToTime = time
|
||||||
}
|
}
|
||||||
@@ -188,7 +192,7 @@ module.exports = class PlaybackController {
|
|||||||
}, 10000) /* give it a few seconds */
|
}, 10000) /* give it a few seconds */
|
||||||
|
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
dispatch('startTorrentingSummary', torrentSummary)
|
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
||||||
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||||
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
||||||
} else {
|
} else {
|
||||||
@@ -241,12 +245,20 @@ module.exports = class PlaybackController {
|
|||||||
return this.update()
|
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
|
// otherwise, play the video
|
||||||
dispatch('setTitle', torrentSummary.files[state.playing.fileIndex].name)
|
|
||||||
this.update()
|
this.update()
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerOpen')
|
ipcRenderer.send('onPlayerOpen')
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -259,8 +271,8 @@ module.exports = class PlaybackController {
|
|||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
Cast.stop()
|
Cast.stop()
|
||||||
}
|
}
|
||||||
if (state.playing.location === 'vlc') {
|
if (state.playing.location === 'external') {
|
||||||
ipcRenderer.send('vlcQuit')
|
ipcRenderer.send('quitExternalPlayer')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save volume (this session only, not in state.saved)
|
// Save volume (this session only, not in state.saved)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const {dispatch} = require('../lib/dispatcher')
|
|
||||||
const State = require('../lib/state')
|
const State = require('../lib/state')
|
||||||
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
// Controls the Preferences screen
|
// Controls the Preferences screen
|
||||||
module.exports = class PrefsController {
|
module.exports = class PrefsController {
|
||||||
@@ -15,11 +16,15 @@ module.exports = class PrefsController {
|
|||||||
url: 'preferences',
|
url: 'preferences',
|
||||||
setup: function (cb) {
|
setup: function (cb) {
|
||||||
// initialize preferences
|
// initialize preferences
|
||||||
dispatch('setTitle', 'Preferences')
|
state.window.title = 'Preferences'
|
||||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||||
|
ipcRenderer.send('setAllowNav', false)
|
||||||
cb()
|
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
|
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||||
save () {
|
save () {
|
||||||
var state = this.state
|
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.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||||
State.save(state)
|
State.save(state)
|
||||||
|
dispatch('checkDownloadPath')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ const fs = require('fs-extra')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const parallel = require('run-parallel')
|
const parallel = require('run-parallel')
|
||||||
|
|
||||||
|
const remote = electron.remote
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class SubtitlesController {
|
module.exports = class SubtitlesController {
|
||||||
@@ -11,7 +13,7 @@ module.exports = class SubtitlesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openSubtitles () {
|
openSubtitles () {
|
||||||
electron.remote.dialog.showOpenDialog({
|
remote.dialog.showOpenDialog({
|
||||||
title: 'Select a subtitles file.',
|
title: 'Select a subtitles file.',
|
||||||
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
||||||
properties: [ 'openFile' ]
|
properties: [ 'openFile' ]
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ module.exports = class TorrentListController {
|
|||||||
// Use path string instead of W3C File object
|
// Use path string instead of W3C File object
|
||||||
torrentId = torrentId.path
|
torrentId = torrentId.path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow a instant.io link to be pasted
|
// Allow a instant.io link to be pasted
|
||||||
// TODO: remove this once support is added to webtorrent core
|
|
||||||
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||||
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||||
}
|
}
|
||||||
@@ -40,12 +40,21 @@ module.exports = class TorrentListController {
|
|||||||
|
|
||||||
// Shows the Create Torrent page with options to seed a given file or folder
|
// Shows the Create Torrent page with options to seed a given file or folder
|
||||||
showCreateTorrent (files) {
|
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
|
// Files will either be an array of file objects, which we can send directly
|
||||||
// to the create-torrent screen
|
// to the create-torrent screen
|
||||||
if (files.length === 0 || typeof files[0] !== 'string') {
|
if (files.length === 0 || typeof files[0] !== 'string') {
|
||||||
this.state.location.go({
|
this.state.location.go({
|
||||||
url: 'create-torrent',
|
url: 'create-torrent',
|
||||||
files: files
|
files: files,
|
||||||
|
setup: (cb) => {
|
||||||
|
this.state.window.title = 'Create New Torrent'
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,27 +76,38 @@ module.exports = class TorrentListController {
|
|||||||
var state = this.state
|
var state = this.state
|
||||||
var torrentKey = state.nextTorrentKey++
|
var torrentKey = state.nextTorrentKey++
|
||||||
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||||
state.location.backToFirst(function () {
|
state.location.cancel()
|
||||||
state.location.clearForward('create-torrent')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts downloading and/or seeding a given torrentSummary.
|
// Starts downloading and/or seeding a given torrentSummary.
|
||||||
startTorrentingSummary (torrentSummary) {
|
startTorrentingSummary (torrentKey) {
|
||||||
var s = torrentSummary
|
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
|
// New torrent: give it a path
|
||||||
if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++
|
if (!s.path) {
|
||||||
|
// Use Downloads folder by default
|
||||||
|
s.path = this.state.saved.prefs.downloadPath
|
||||||
|
return start()
|
||||||
|
}
|
||||||
|
|
||||||
// Use Downloads folder by default
|
// Existing torrent: check that the path is still there
|
||||||
if (!s.path) s.path = this.state.saved.prefs.downloadPath
|
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
|
||||||
|
if (err) {
|
||||||
|
s.error = 'path-missing'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start()
|
||||||
|
})
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-torrenting',
|
function start () {
|
||||||
s.torrentKey,
|
ipcRenderer.send('wt-start-torrenting',
|
||||||
TorrentSummary.getTorrentID(s),
|
s.torrentKey,
|
||||||
s.path,
|
TorrentSummary.getTorrentID(s),
|
||||||
s.fileModtimes,
|
s.path,
|
||||||
s.selections)
|
s.fileModtimes,
|
||||||
|
s.selections)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
@@ -95,7 +115,7 @@ module.exports = class TorrentListController {
|
|||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
torrentSummary.status = 'new'
|
torrentSummary.status = 'new'
|
||||||
this.startTorrentingSummary(torrentSummary)
|
this.startTorrentingSummary(torrentSummary.torrentKey)
|
||||||
sound.play('ENABLE')
|
sound.play('ENABLE')
|
||||||
} else {
|
} else {
|
||||||
torrentSummary.status = 'paused'
|
torrentSummary.status = 'paused'
|
||||||
@@ -252,7 +272,7 @@ function deleteFile (path) {
|
|||||||
// Delete all files in a torrent
|
// Delete all files in a torrent
|
||||||
function moveItemToTrash (torrentSummary) {
|
function moveItemToTrash (torrentSummary) {
|
||||||
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
ipcRenderer.send('moveItemToTrash', filePath)
|
if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function showItemInFolder (torrentSummary) {
|
function showItemInFolder (torrentSummary) {
|
||||||
@@ -271,6 +291,7 @@ function saveTorrentFileAs (torrentSummary) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
||||||
|
if (!savePath) return // They clicked Cancel
|
||||||
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||||
if (err) return dispatch('error', err)
|
if (err) return dispatch('error', err)
|
||||||
|
|||||||
@@ -396,7 +396,9 @@ function stop () {
|
|||||||
|
|
||||||
function stoppedCasting () {
|
function stoppedCasting () {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.playing.jumpToTime = state.playing.currentTime
|
state.playing.jumpToTime = Number.isFinite(state.playing.currentTime)
|
||||||
|
? state.playing.currentTime
|
||||||
|
: 0
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ module.exports = {
|
|||||||
run
|
run
|
||||||
}
|
}
|
||||||
|
|
||||||
var semver = require('semver')
|
const semver = require('semver')
|
||||||
var config = require('../../config')
|
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
|
// 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
|
// 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)
|
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
|
// Config is now on the new version
|
||||||
state.saved.version = config.APP_VERSION
|
state.saved.version = config.APP_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_7_0 (saved) {
|
function migrate_0_7_0 (saved) {
|
||||||
console.log('migrate to 0.7.0')
|
|
||||||
|
|
||||||
var fs = require('fs-extra')
|
var fs = require('fs-extra')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
@@ -46,7 +54,6 @@ function migrate_0_7_0 (saved) {
|
|||||||
// * Finally, now we're getting rid of torrentPath altogether
|
// * Finally, now we're getting rid of torrentPath altogether
|
||||||
var src, dst
|
var src, dst
|
||||||
if (ts.torrentPath) {
|
if (ts.torrentPath) {
|
||||||
console.log('replacing torrentPath %s', ts.torrentPath)
|
|
||||||
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||||
src = ts.torrentPath
|
src = ts.torrentPath
|
||||||
} else {
|
} else {
|
||||||
@@ -63,7 +70,6 @@ function migrate_0_7_0 (saved) {
|
|||||||
|
|
||||||
// Replace posterURL with posterFileName
|
// Replace posterURL with posterFileName
|
||||||
if (ts.posterURL) {
|
if (ts.posterURL) {
|
||||||
console.log('replacing posterURL %s', ts.posterURL)
|
|
||||||
var extension = path.extname(ts.posterURL)
|
var extension = path.extname(ts.posterURL)
|
||||||
src = path.isAbsolute(ts.posterURL)
|
src = path.isAbsolute(ts.posterURL)
|
||||||
? ts.posterURL
|
? ts.posterURL
|
||||||
@@ -87,9 +93,45 @@ function migrate_0_7_0 (saved) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_7_2 (saved) {
|
function migrate_0_7_2 (saved) {
|
||||||
if (!saved.prefs) {
|
if (saved.prefs == null) {
|
||||||
saved.prefs = {
|
saved.prefs = {
|
||||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
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 = {
|
var saved = {
|
||||||
prefs: {
|
prefs: {
|
||||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
||||||
|
isFileHandler: false,
|
||||||
|
openExternalPlayer: false,
|
||||||
|
externalPlayerPath: null
|
||||||
},
|
},
|
||||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||||
@@ -200,6 +203,9 @@ function save (state, cb) {
|
|||||||
if (key === 'playStatus') {
|
if (key === 'playStatus') {
|
||||||
continue // Don't save whether a torrent is playing / pending
|
continue // Don't save whether a torrent is playing / pending
|
||||||
}
|
}
|
||||||
|
if (key === 'error') {
|
||||||
|
continue // Don't save error states
|
||||||
|
}
|
||||||
torrent[key] = x[key]
|
torrent[key] = x[key]
|
||||||
}
|
}
|
||||||
return torrent
|
return torrent
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function init (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var now = new Date()
|
var now = new Date()
|
||||||
|
telemetry.version = config.APP_VERSION
|
||||||
telemetry.timestamp = now.toISOString()
|
telemetry.timestamp = now.toISOString()
|
||||||
telemetry.localTime = now.toTimeString()
|
telemetry.localTime = now.toTimeString()
|
||||||
telemetry.screens = getScreenInfo()
|
telemetry.screens = getScreenInfo()
|
||||||
@@ -32,6 +33,8 @@ function init (state) {
|
|||||||
|
|
||||||
if (config.IS_PRODUCTION) {
|
if (config.IS_PRODUCTION) {
|
||||||
postToServer()
|
postToServer()
|
||||||
|
// If the user keeps WebTorrent running for a long time, post every 24h
|
||||||
|
setInterval(postToServer, 12 * 3600 * 1000)
|
||||||
} else {
|
} else {
|
||||||
// Development: telemetry used only for local debugging
|
// Development: telemetry used only for local debugging
|
||||||
// Empty uncaught errors, etc at the start of every run
|
// 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
|
// An uncaught error happened in the main process or in one of the windows
|
||||||
function logUncaughtError (procName, err) {
|
function logUncaughtError (procName, err) {
|
||||||
console.error('uncaught error', procName, err)
|
|
||||||
|
|
||||||
// Not initialized yet? Ignore.
|
// Not initialized yet? Ignore.
|
||||||
// Hopefully uncaught errors immediately on startup are fixed in dev
|
// Hopefully uncaught errors immediately on startup are fixed in dev
|
||||||
if (!telemetry) return
|
if (!telemetry) return
|
||||||
@@ -125,7 +126,10 @@ function logUncaughtError (procName, err) {
|
|||||||
var message, stack
|
var message, stack
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
message = err.message
|
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 {
|
} else {
|
||||||
message = String(err)
|
message = String(err)
|
||||||
stack = ''
|
stack = ''
|
||||||
|
|||||||
@@ -52,5 +52,6 @@ function getByKey (state, torrentKey) {
|
|||||||
// module. Store root folder explicitly to avoid hacky path processing below.
|
// module. Store root folder explicitly to avoid hacky path processing below.
|
||||||
function getFileOrFolder (torrentSummary) {
|
function getFileOrFolder (torrentSummary) {
|
||||||
var ts = 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])
|
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 electron = require('electron')
|
||||||
const React = require('react')
|
const React = require('react')
|
||||||
const ReactDOM = require('react-dom')
|
const ReactDOM = require('react-dom')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
const config = require('../config')
|
const config = require('../config')
|
||||||
const App = require('./views/app')
|
const App = require('./views/app')
|
||||||
@@ -77,17 +78,20 @@ function onState (err, _state) {
|
|||||||
// Restart everything we were torrenting last time the app ran
|
// Restart everything we were torrenting last time the app ran
|
||||||
resumeTorrents()
|
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
|
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
||||||
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
|
|
||||||
// Listen for messages from the main process
|
// Listen for messages from the main process
|
||||||
setupIpc()
|
setupIpc()
|
||||||
|
|
||||||
// Calling update() updates the UI given the current state
|
// Warn if the download dir is gone, eg b/c an external drive is unplugged
|
||||||
// Do this at least once a second to give every file in every torrentSummary
|
checkDownloadPath()
|
||||||
// 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'))
|
|
||||||
|
|
||||||
// OS integrations:
|
// OS integrations:
|
||||||
// ...drag and drop files/text to start torrenting or seeding
|
// ...drag and drop files/text to start torrenting or seeding
|
||||||
@@ -175,8 +179,7 @@ const dispatchHandlers = {
|
|||||||
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
||||||
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
||||||
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
||||||
'startTorrentingSummary': (torrentSummary) =>
|
'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
|
||||||
controllers.torrentList.startTorrentingSummary(torrentSummary),
|
|
||||||
|
|
||||||
// Playback
|
// Playback
|
||||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
||||||
@@ -195,14 +198,14 @@ const dispatchHandlers = {
|
|||||||
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),
|
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),
|
||||||
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect),
|
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect),
|
||||||
|
|
||||||
// Local media: <video>, <audio>, VLC
|
// Local media: <video>, <audio>, external players
|
||||||
'mediaStalled': () => controllers.media.mediaStalled(),
|
'mediaStalled': () => controllers.media.mediaStalled(),
|
||||||
'mediaError': (err) => controllers.media.mediaError(err),
|
'mediaError': (err) => controllers.media.mediaError(err),
|
||||||
'mediaSuccess': () => controllers.media.mediaSuccess(),
|
'mediaSuccess': () => controllers.media.mediaSuccess(),
|
||||||
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
|
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
|
||||||
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
|
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
|
||||||
'vlcPlay': () => controllers.media.vlcPlay(),
|
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
|
||||||
'vlcNotFound': () => controllers.media.vlcNotFound(),
|
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
|
||||||
|
|
||||||
// Remote casting: Chromecast, Airplay, etc
|
// Remote casting: Chromecast, Airplay, etc
|
||||||
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
|
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
|
||||||
@@ -212,6 +215,7 @@ const dispatchHandlers = {
|
|||||||
// Preferences screen
|
// Preferences screen
|
||||||
'preferences': () => controllers.prefs.show(),
|
'preferences': () => controllers.prefs.show(),
|
||||||
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
|
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
|
||||||
|
'checkDownloadPath': checkDownloadPath,
|
||||||
|
|
||||||
// Update (check for new versions on Linux, where there's no auto updater)
|
// Update (check for new versions on Linux, where there's no auto updater)
|
||||||
'updateAvailable': (version) => controllers.update.updateAvailable(version),
|
'updateAvailable': (version) => controllers.update.updateAvailable(version),
|
||||||
@@ -223,6 +227,7 @@ const dispatchHandlers = {
|
|||||||
'escapeBack': escapeBack,
|
'escapeBack': escapeBack,
|
||||||
'back': () => state.location.back(),
|
'back': () => state.location.back(),
|
||||||
'forward': () => state.location.forward(),
|
'forward': () => state.location.forward(),
|
||||||
|
'cancel': () => state.location.cancel(),
|
||||||
|
|
||||||
// Controlling the window
|
// Controlling the window
|
||||||
'setDimensions': setDimensions,
|
'setDimensions': setDimensions,
|
||||||
@@ -312,8 +317,14 @@ function escapeBack () {
|
|||||||
// Starts all torrents that aren't paused on program startup
|
// Starts all torrents that aren't paused on program startup
|
||||||
function resumeTorrents () {
|
function resumeTorrents () {
|
||||||
state.saved.torrents
|
state.saved.torrents
|
||||||
.filter((torrentSummary) => torrentSummary.status !== 'paused')
|
.map((torrentSummary) => {
|
||||||
.forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(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
|
// Set window dimensions to match video dimensions or fill the screen
|
||||||
@@ -351,7 +362,7 @@ function setDimensions (dimensions) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
ipcRenderer.send('setAspectRatio', aspectRatio)
|
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
|
state.playing.aspectRatio = aspectRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,25 +371,25 @@ function setDimensions (dimensions) {
|
|||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
if (!Array.isArray(files)) files = [ 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
|
state.modal = null
|
||||||
}
|
controllers.torrentList.showCreateTorrent(files)
|
||||||
|
} else {
|
||||||
var subtitles = files.filter(controllers.subtitles.isSubtitle)
|
// Drop files onto any other screen: show error
|
||||||
|
return onError('Please go back to the torrent list before creating a new torrent.')
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update()
|
update()
|
||||||
@@ -432,3 +443,14 @@ function onFullscreenChanged (e, isFullScreen) {
|
|||||||
|
|
||||||
update()
|
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]
|
var ModalContents = Modals[state.modal.id]
|
||||||
return (
|
return (
|
||||||
<div key='modal' className='modal'>
|
<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'>
|
<div key='modal-content' className='modal-content'>
|
||||||
<ModalContents state={state} />
|
<ModalContents state={state} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module.exports = class CreateTorrentErrorPage extends React.Component {
|
|||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
<p className='float-right'>
|
<p className='float-right'>
|
||||||
<button className='button-flat light' onClick={dispatcher('back')}>
|
<button className='button-flat light' onClick={dispatcher('cancel')}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -73,11 +73,11 @@ module.exports = class CreateTorrentPage extends React.Component {
|
|||||||
<div key='advanced' className={'create-torrent-advanced ' + collapsedClass}>
|
<div key='advanced' className={'create-torrent-advanced ' + collapsedClass}>
|
||||||
<div key='comment' className='torrent-attribute'>
|
<div key='comment' className='torrent-attribute'>
|
||||||
<label>Comment:</label>
|
<label>Comment:</label>
|
||||||
<textarea className='torrent-attribute torrent-comment'></textarea>
|
<textarea className='torrent-attribute torrent-comment' />
|
||||||
</div>
|
</div>
|
||||||
<div key='trackers' className='torrent-attribute'>
|
<div key='trackers' className='torrent-attribute'>
|
||||||
<label>Trackers:</label>
|
<label>Trackers:</label>
|
||||||
<textarea className='torrent-attribute torrent-trackers' value={trackers}></textarea>
|
<textarea className='torrent-attribute torrent-trackers' defaultValue={trackers} />
|
||||||
</div>
|
</div>
|
||||||
<div key='private' className='torrent-attribute'>
|
<div key='private' className='torrent-attribute'>
|
||||||
<label>Private:</label>
|
<label>Private:</label>
|
||||||
@@ -89,7 +89,7 @@ module.exports = class CreateTorrentPage extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div key='buttons' className='float-right'>
|
<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>
|
<button key='create' className='button-raised' onClick={handleOK}>Create Torrent</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const React = require('react')
|
|||||||
const Bitfield = require('bitfield')
|
const Bitfield = require('bitfield')
|
||||||
const prettyBytes = require('prettier-bytes')
|
const prettyBytes = require('prettier-bytes')
|
||||||
const zeroFill = require('zero-fill')
|
const zeroFill = require('zero-fill')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
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
|
// Align the title with the other info, if available. Otherwise, center title
|
||||||
var emptyLabel = (<label></label>)
|
var emptyLabel = (<label />)
|
||||||
elems.unshift((
|
elems.unshift((
|
||||||
<div key='title' className='audio-title'>
|
<div key='title' className='audio-title'>
|
||||||
{elems.length ? emptyLabel : undefined}{title}
|
{elems.length ? emptyLabel : undefined}{title}
|
||||||
@@ -281,9 +282,12 @@ function renderCastScreen (state) {
|
|||||||
castIcon = 'tv'
|
castIcon = 'tv'
|
||||||
castType = 'DLNA'
|
castType = 'DLNA'
|
||||||
isCast = true
|
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'
|
castIcon = 'tv'
|
||||||
castType = 'VLC'
|
castType = playerName
|
||||||
isCast = false
|
isCast = false
|
||||||
} else if (state.playing.location === 'error') {
|
} else if (state.playing.location === 'error') {
|
||||||
castIcon = 'error_outline'
|
castIcon = 'error_outline'
|
||||||
@@ -380,16 +384,16 @@ function renderPlayerControls (state) {
|
|||||||
<div
|
<div
|
||||||
key='cursor'
|
key='cursor'
|
||||||
className='playback-cursor'
|
className='playback-cursor'
|
||||||
style={playbackCursorStyle}>
|
style={playbackCursorStyle}
|
||||||
</div>
|
/>
|
||||||
<div
|
<div
|
||||||
key='scrub-bar'
|
key='scrub-bar'
|
||||||
className='scrub-bar'
|
className='scrub-bar'
|
||||||
draggable='true'
|
draggable='true'
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onClick={handleScrub}
|
onClick={handleScrub}
|
||||||
onDrag={handleScrub}>
|
onDrag={handleScrub}
|
||||||
</div>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
|
|
||||||
<i
|
<i
|
||||||
@@ -593,7 +597,7 @@ function renderLoadingBar (state) {
|
|||||||
width: (100 * part.count / fileProg.numPieces) + '%'
|
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>)
|
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const React = require('react')
|
const React = require('react')
|
||||||
const remote = require('electron').remote
|
const remote = require('electron').remote
|
||||||
const dialog = remote.dialog
|
const dialog = remote.dialog
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ module.exports = class Preferences extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className='preferences'>
|
<div className='preferences'>
|
||||||
{renderGeneralSection(state)}
|
{renderGeneralSection(state)}
|
||||||
|
{renderPlaybackSection(state)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -22,11 +24,23 @@ function renderGeneralSection (state) {
|
|||||||
description: '',
|
description: '',
|
||||||
icon: 'settings'
|
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({
|
return renderFileSelector({
|
||||||
key: 'download-path',
|
key: 'download-path',
|
||||||
label: 'Download Path',
|
label: 'Download Path',
|
||||||
@@ -39,7 +53,65 @@ function renderDownloadDirSelector (state) {
|
|||||||
},
|
},
|
||||||
state.unsaved.prefs.downloadPath,
|
state.unsaved.prefs.downloadPath,
|
||||||
function (filePath) {
|
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
|
// Creates a file chooser
|
||||||
// - defition should be {label, description, options}
|
// - defition should be {label, description, options}
|
||||||
// options are passed to dialog.showOpenDialog
|
// options are passed to dialog.showOpenDialog
|
||||||
// - value should be the current pref, a file or folder path
|
// - value should be the current pref, a file or folder path
|
||||||
// - callback takes a new file or folder path
|
// - callback takes a new file or folder path
|
||||||
function renderFileSelector (definition, value, callback) {
|
function renderFileSelector (definition, value, callback) {
|
||||||
return (
|
var controls = [(
|
||||||
<div key={definition.key} className='control-group'>
|
<input
|
||||||
<div className='controls'>
|
type='text'
|
||||||
<label className='control-label'>
|
className='file-picker-text'
|
||||||
<div className='preference-title'>{definition.label}</div>
|
key={definition.property}
|
||||||
<div className='preference-description'>{definition.description}</div>
|
id={definition.property}
|
||||||
</label>
|
disabled='disabled'
|
||||||
<div className='controls'>
|
value={value} />
|
||||||
<input type='text' className='file-picker-text'
|
), (
|
||||||
id={definition.property}
|
<button
|
||||||
disabled='disabled'
|
key={definition.property + '-btn'}
|
||||||
value={value} />
|
className='btn'
|
||||||
<button className='btn' onClick={handleClick}>
|
onClick={handleClick}>
|
||||||
<i className='icon'>folder_open</i>
|
<i className='icon'>folder_open</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)]
|
||||||
</div>
|
return renderControlGroup(definition, controls)
|
||||||
</div>
|
|
||||||
)
|
|
||||||
function handleClick () {
|
function handleClick () {
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(filenames)) return
|
||||||
@@ -100,6 +200,18 @@ function renderFileSelector (definition, value, callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStateValue (property, value) {
|
function renderControlGroup (definition, controls) {
|
||||||
dispatch('updatePreferences', property, value)
|
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 {
|
module.exports = class TorrentList extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
var state = this.props.state
|
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)
|
(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 (
|
return (
|
||||||
<div key='torrent-list' className='torrent-list'>
|
<div key='torrent-list' className='torrent-list'>
|
||||||
{torrentRows}
|
{contents}
|
||||||
<div key='torrent-placeholder' className='torrent-placeholder'>
|
|
||||||
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -44,6 +60,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
||||||
if (isSelected) classes.push('selected')
|
if (isSelected) classes.push('selected')
|
||||||
if (!infoHash) classes.push('disabled')
|
if (!infoHash) classes.push('disabled')
|
||||||
|
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={torrentSummary.torrentKey}
|
key={torrentSummary.torrentKey}
|
||||||
@@ -67,8 +84,14 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
|
|
||||||
// If it's downloading/seeding then show progress info
|
// If it's downloading/seeding then show progress info
|
||||||
var prog = torrentSummary.progress
|
var prog = torrentSummary.progress
|
||||||
if (torrentSummary.status !== 'paused' && prog) {
|
if (torrentSummary.error) {
|
||||||
elements.push((
|
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'>
|
<div key='progress-info' className='ellipsis'>
|
||||||
{renderPercentProgress()}
|
{renderPercentProgress()}
|
||||||
{renderTotalProgress()}
|
{renderTotalProgress()}
|
||||||
@@ -77,7 +100,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
{renderUploadSpeed()}
|
{renderUploadSpeed()}
|
||||||
{renderEta()}
|
{renderEta()}
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div key='metadata' className='metadata'>{elements}</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
|
// Only show the play button for torrents that contain playable media
|
||||||
var playButton
|
var playButton, downloadButton
|
||||||
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
var noErrors = !torrentSummary.error
|
||||||
|
if (noErrors && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||||
playButton = (
|
playButton = (
|
||||||
<i
|
<i
|
||||||
key='play-button'
|
key='play-button'
|
||||||
@@ -186,11 +210,8 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
</i>
|
</i>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (noErrors) {
|
||||||
return (
|
downloadButton = (
|
||||||
<div key='buttons' className='buttons'>
|
|
||||||
{positionElem}
|
|
||||||
{playButton}
|
|
||||||
<i
|
<i
|
||||||
key='download-button'
|
key='download-button'
|
||||||
className={'button-round icon download ' + torrentSummary.status}
|
className={'button-round icon download ' + torrentSummary.status}
|
||||||
@@ -198,6 +219,14 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
onClick={dispatcher('toggleTorrent', infoHash)}>
|
onClick={dispatcher('toggleTorrent', infoHash)}>
|
||||||
{downloadIcon}
|
{downloadIcon}
|
||||||
</i>
|
</i>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key='buttons' className='buttons'>
|
||||||
|
{positionElem}
|
||||||
|
{playButton}
|
||||||
|
{downloadButton}
|
||||||
<i
|
<i
|
||||||
key='delete-button'
|
key='delete-button'
|
||||||
className='icon delete'
|
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
|
// Show files, per-file download status and play buttons, and so on
|
||||||
renderTorrentDetails (torrentSummary) {
|
renderTorrentDetails (torrentSummary) {
|
||||||
var filesElement
|
var filesElement
|
||||||
if (!torrentSummary.files) {
|
if (torrentSummary.error || !torrentSummary.files) {
|
||||||
// We don't know what files this torrent contains
|
var message = ''
|
||||||
var message = torrentSummary.status === 'paused'
|
if (torrentSummary.error === 'path-missing') {
|
||||||
? 'Failed to load torrent info. Click the download button to try again...'
|
// Special case error: this torrent's download dir or file is missing
|
||||||
: 'Downloading torrent info...'
|
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
filesElement = (<div key='files' className='files warning'>{message}</div>)
|
} 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 {
|
} else {
|
||||||
// We do know the files. List them and show download stats for each one
|
// We do know the files. List them and show download stats for each one
|
||||||
var fileRows = torrentSummary.files
|
var fileRows = torrentSummary.files
|
||||||
@@ -316,15 +359,28 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
<div key='radial-progress' className={'radial-progress ' + cssClass}>
|
<div key='radial-progress' className={'radial-progress ' + cssClass}>
|
||||||
<div key='circle' className='circle'>
|
<div key='circle' className='circle'>
|
||||||
<div key='mask-full' className='mask full' style={transformFill}>
|
<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>
|
||||||
<div key='mask-half' className='mask half'>
|
<div key='mask-half' className='mask half'>
|
||||||
<div key='fill' className='fill' style={transformFill}></div>
|
<div key='fill' className='fill' style={transformFill} />
|
||||||
<div key='fill-fix' className='fill fix' style={transformFix}></div>
|
<div key='fill-fix' className='fill fix' style={transformFix} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div key='inset' className='inset'></div>
|
<div key='inset' className='inset' />
|
||||||
</div>
|
</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 React = require('react')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
const {dispatcher} = require('../lib/dispatcher')
|
const {dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
@@ -10,11 +11,15 @@ module.exports = class UnsupportedMediaModal extends React.Component {
|
|||||||
var message = (err && err.getMessage)
|
var message = (err && err.getMessage)
|
||||||
? err.getMessage()
|
? err.getMessage()
|
||||||
: err
|
: err
|
||||||
var actionButton = state.modal.vlcInstalled
|
var playerPath = state.saved.prefs.externalPlayerPath
|
||||||
? (<button className='button-raised' onClick={dispatcher('vlcPlay')}>Play in VLC</button>)
|
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>)
|
: (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>)
|
||||||
var vlcMessage = state.modal.vlcNotFound
|
var playerMessage = state.modal.externalPlayerNotFound
|
||||||
? 'Couldn\'t run VLC. Please make sure it\'s installed.'
|
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
||||||
: ''
|
: ''
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -24,7 +29,7 @@ module.exports = class UnsupportedMediaModal extends React.Component {
|
|||||||
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
|
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
|
||||||
{actionButton}
|
{actionButton}
|
||||||
</p>
|
</p>
|
||||||
<p className='error-text'>{vlcMessage}</p>
|
<p className='error-text'>{playerMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -34,6 +39,6 @@ module.exports = class UnsupportedMediaModal extends React.Component {
|
|||||||
|
|
||||||
// TODO: dcposch send a dispatch rather than modifying state directly
|
// TODO: dcposch send a dispatch rather than modifying state directly
|
||||||
var state = this.props.state
|
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><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 className='float-right'>
|
<p className='float-right'>
|
||||||
<button className='button button-flat' onClick={handleCancel}>Skip This Release</button>
|
<button className='button button-flat' onClick={handleSkip}>Skip This Release</button>
|
||||||
<button className='button button-raised' onClick={handleOK}>Show Download Page</button>
|
<button className='button button-raised' onClick={handleShow}>Show Download Page</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
function handleOK () {
|
function handleShow () {
|
||||||
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
|
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
|
||||||
dispatch('exitModal')
|
dispatch('exitModal')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel () {
|
function handleSkip () {
|
||||||
dispatch('skipVersion', state.modal.version)
|
dispatch('skipVersion', state.modal.version)
|
||||||
dispatch('exitModal')
|
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
|
* UTILITY CLASSES
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.ellipsis {
|
.ellipsis {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -551,6 +555,19 @@ input[type='text'] {
|
|||||||
line-height: 1.5em;
|
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
|
* TORRENT LIST: DRAG-DROP TARGET
|
||||||
*/
|
*/
|
||||||
@@ -919,6 +936,10 @@ video::-webkit-media-text-track-container {
|
|||||||
margin-right: 0.2em;
|
margin-right: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preferences .icon.enabled {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
.preferences .btn {
|
.preferences .btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
-webkit-appearance: button;
|
-webkit-appearance: button;
|
||||||
@@ -1066,6 +1087,14 @@ video::-webkit-media-text-track-container {
|
|||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preferences .checkbox {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MEDIA OVERLAY / AUDIO DETAILS
|
* MEDIA OVERLAY / AUDIO DETAILS
|
||||||
|
|||||||
Reference in New Issue
Block a user