Compare commits
29 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 | ||
|
|
6c68645b0f |
@@ -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)
|
||||||
|
|||||||
@@ -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.11.0",
|
"version": "0.12.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@webtorrent.io",
|
"email": "feross@webtorrent.io",
|
||||||
|
|||||||
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()
|
||||||
|
|
||||||
|
|||||||
@@ -34,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()
|
||||||
@@ -159,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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,16 +14,13 @@ 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
|
||||||
|
|
||||||
@@ -115,52 +112,17 @@ function init () {
|
|||||||
ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...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
|
||||||
|
|||||||
@@ -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,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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -143,7 +143,11 @@ function setBounds (bounds, maximize) {
|
|||||||
}
|
}
|
||||||
// Resize the window's content area (so window border doesn't need to be taken
|
// Resize the window's content area (so window border doesn't need to be taken
|
||||||
// into account)
|
// into account)
|
||||||
main.win.setContentBounds(bounds, true)
|
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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -241,16 +245,17 @@ 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)
|
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
||||||
if (this.state.saved.prefs.playInVlc) {
|
if (this.state.saved.prefs.openExternalPlayer) {
|
||||||
dispatch('vlcPlay')
|
dispatch('openExternalPlayer')
|
||||||
this.update()
|
this.update()
|
||||||
cb()
|
cb()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, play the video
|
// otherwise, play the video
|
||||||
state.window.title = torrentSummary.files[state.playing.fileIndex].name
|
|
||||||
this.update()
|
this.update()
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerOpen')
|
ipcRenderer.send('onPlayerOpen')
|
||||||
@@ -266,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)
|
||||||
|
|||||||
@@ -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' ]
|
||||||
|
|||||||
@@ -84,21 +84,30 @@ module.exports = class TorrentListController {
|
|||||||
var s = TorrentSummary.getByKey(this.state, torrentKey)
|
var s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
if (!s) throw new Error('Missing key: ' + torrentKey)
|
if (!s) throw new Error('Missing key: ' + torrentKey)
|
||||||
|
|
||||||
// Use Downloads folder by default
|
// New torrent: give it a path
|
||||||
if (!s.path) s.path = this.state.saved.prefs.downloadPath
|
if (!s.path) {
|
||||||
|
// Use Downloads folder by default
|
||||||
|
s.path = this.state.saved.prefs.downloadPath
|
||||||
|
return start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing torrent: check that the path is still there
|
||||||
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
|
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
s.error = 'path-missing'
|
s.error = 'path-missing'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
start()
|
||||||
|
})
|
||||||
|
|
||||||
|
function start () {
|
||||||
ipcRenderer.send('wt-start-torrenting',
|
ipcRenderer.send('wt-start-torrenting',
|
||||||
s.torrentKey,
|
s.torrentKey,
|
||||||
TorrentSummary.getTorrentID(s),
|
TorrentSummary.getTorrentID(s),
|
||||||
s.path,
|
s.path,
|
||||||
s.fileModtimes,
|
s.fileModtimes,
|
||||||
s.selections)
|
s.selections)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
@@ -263,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) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -29,13 +31,15 @@ function run (state) {
|
|||||||
migrate_0_11_0(state.saved)
|
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')
|
||||||
|
|
||||||
@@ -50,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 {
|
||||||
@@ -67,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
|
||||||
@@ -91,7 +93,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -99,8 +101,37 @@ function migrate_0_7_2 (saved) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_11_0 (saved) {
|
function migrate_0_11_0 (saved) {
|
||||||
if (saved.prefs.isFileHandler === undefined) {
|
if (saved.prefs.isFileHandler == null) {
|
||||||
// The app used to make itself the default torrent file handler automatically
|
// The app used to make itself the default torrent file handler automatically
|
||||||
saved.prefs.isFileHandler = true
|
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 */
|
||||||
|
|||||||
@@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,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),
|
||||||
@@ -362,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
@@ -34,24 +35,11 @@ function renderPlaybackSection (state) {
|
|||||||
description: '',
|
description: '',
|
||||||
icon: 'settings'
|
icon: 'settings'
|
||||||
}, [
|
}, [
|
||||||
renderPlayInVlcSelector(state)
|
renderOpenExternalPlayerSelector(state),
|
||||||
|
renderExternalPlayerSelector(state)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPlayInVlcSelector (state) {
|
|
||||||
return renderCheckbox({
|
|
||||||
key: 'play-in-vlc',
|
|
||||||
label: 'Play in VLC',
|
|
||||||
description: 'Media will play in VLC',
|
|
||||||
property: 'playInVlc',
|
|
||||||
value: state.saved.prefs.playInVlc
|
|
||||||
},
|
|
||||||
state.unsaved.prefs.playInVlc,
|
|
||||||
function (value) {
|
|
||||||
dispatch('updatePreferences', 'playInVlc', value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDownloadPathSelector (state) {
|
function renderDownloadPathSelector (state) {
|
||||||
return renderFileSelector({
|
return renderFileSelector({
|
||||||
key: 'download-path',
|
key: 'download-path',
|
||||||
@@ -92,6 +80,41 @@ function renderFileHandlers (state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Renders a prefs section.
|
// Renders a prefs section.
|
||||||
// - definition should be {icon, title, description}
|
// - definition should be {icon, title, description}
|
||||||
// - controls should be an array of vdom elements
|
// - controls should be an array of vdom elements
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 |
Reference in New Issue
Block a user