Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
dc7ccb3956 | ||
|
|
a420936657 | ||
|
|
dcab7f72d4 | ||
|
|
a695f7c2d7 | ||
|
|
7677bff6d4 | ||
|
|
c7626997de | ||
|
|
91a1ab4a56 | ||
|
|
3e19cdfb0b | ||
|
|
2043dc2161 | ||
|
|
a9e36472c5 | ||
|
|
4df4f9b2ad | ||
|
|
4ad55173a5 | ||
|
|
b9c82dd6b2 | ||
|
|
8333f4893f | ||
|
|
f071965ae8 | ||
|
|
a4fa9ac666 | ||
|
|
939ee555b7 |
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
|
||||||
|
|||||||
@@ -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.11.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"
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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')
|
||||||
@@ -60,14 +61,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 +92,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,6 +112,7 @@ 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
|
* VLC
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ 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)
|
||||||
|
main.win.setContentBounds(bounds, true)
|
||||||
} else {
|
} else {
|
||||||
log('setBounds: not setting bounds because of window maximization')
|
log('setBounds: not setting bounds because of window maximization')
|
||||||
}
|
}
|
||||||
@@ -204,13 +206,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 () {
|
||||||
|
|||||||
@@ -188,7 +188,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 +241,19 @@ module.exports = class PlaybackController {
|
|||||||
return this.update()
|
return this.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
||||||
|
if (this.state.saved.prefs.playInVlc) {
|
||||||
|
dispatch('vlcPlay')
|
||||||
|
this.update()
|
||||||
|
cb()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// otherwise, play the video
|
// otherwise, play the video
|
||||||
dispatch('setTitle', torrentSummary.files[state.playing.fileIndex].name)
|
state.window.title = torrentSummary.files[state.playing.fileIndex].name
|
||||||
this.update()
|
this.update()
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerOpen')
|
ipcRenderer.send('onPlayerOpen')
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,29 @@ 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
|
|
||||||
if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++
|
|
||||||
|
|
||||||
// Use Downloads folder by default
|
// Use Downloads folder by default
|
||||||
if (!s.path) s.path = this.state.saved.prefs.downloadPath
|
if (!s.path) s.path = this.state.saved.prefs.downloadPath
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-torrenting',
|
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
|
||||||
s.torrentKey,
|
if (err) {
|
||||||
TorrentSummary.getTorrentID(s),
|
s.error = 'path-missing'
|
||||||
s.path,
|
return
|
||||||
s.fileModtimes,
|
}
|
||||||
s.selections)
|
ipcRenderer.send('wt-start-torrenting',
|
||||||
|
s.torrentKey,
|
||||||
|
TorrentSummary.getTorrentID(s),
|
||||||
|
s.path,
|
||||||
|
s.fileModtimes,
|
||||||
|
s.selections)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
@@ -95,7 +106,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'
|
||||||
@@ -271,6 +282,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)
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -93,3 +97,10 @@ function migrate_0_7_2 (saved) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrate_0_11_0 (saved) {
|
||||||
|
if (saved.prefs.isFileHandler === undefined) {
|
||||||
|
// The app used to make itself the default torrent file handler automatically
|
||||||
|
saved.prefs.isFileHandler = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -200,6 +200,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
|
||||||
|
|||||||
@@ -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),
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -232,7 +232,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}
|
||||||
@@ -380,16 +380,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 +593,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>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,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 +23,36 @@ 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'
|
||||||
|
}, [
|
||||||
|
renderPlayInVlcSelector(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) {
|
||||||
return renderFileSelector({
|
return renderFileSelector({
|
||||||
key: 'download-path',
|
key: 'download-path',
|
||||||
label: 'Download Path',
|
label: 'Download Path',
|
||||||
@@ -39,10 +65,33 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -67,31 +116,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 +177,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'
|
||||||
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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