diff --git a/AUTHORS.md b/AUTHORS.md
index 034d67e2..b103fd34 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -2,29 +2,30 @@
#### Ordered by first contribution.
-- Feross Aboukhadijeh
-- DC
-- Nate Goldman
-- Chris Morris
-- Giuseppe Crinò
-- Romain Beaumont
-- Dan Flettre
-- Liam Gray
-- grunjol
-- Rémi Jouannet
-- Evan Miller
-- Alex
-- Diego Rodríguez Baquero
-- Karlo Luis Martinez Martos
-- gabriel
-- Rolando Guedes
-- Benjamin Tan
-- Mathias Rasmussen
-- Sergey Bargamon
-- Thomas Watson Steen
-- anonymlol
-- Gediminas Petrikas
-- Adam Gotlib
-- Rémi Jouannet
+- Feross Aboukhadijeh (feross@feross.org)
+- DC (dcposch@dcpos.ch)
+- Nate Goldman (nate@ngoldman.me)
+- Chris Morris (chris@chrismorris.org)
+- Giuseppe Crinò (giuscri@gmail.com)
+- Romain Beaumont (romain.rom1@gmail.com)
+- Dan Flettre (fletd01@yahoo.com)
+- Liam Gray (liam.r.gray@gmail.com)
+- grunjol (grunjol@users.noreply.github.com)
+- Rémi Jouannet (remijouannet@users.noreply.github.com)
+- Evan Miller (miller.evan815@gmail.com)
+- Alex (alxmorais8@msn.com)
+- Diego Rodríguez Baquero (diegorbaquero@gmail.com)
+- Karlo Luis Martinez Martos (karlo.luis.m@gmail.com)
+- gabriel (furstenheim@gmail.com)
+- Rolando Guedes (rolando.guedes@3gnt.net)
+- Benjamin Tan (demoneaux@gmail.com)
+- Mathias Rasmussen (mathiasvr@gmail.com)
+- Sergey Bargamon (sergey@bargamon.ru)
+- Thomas Watson Steen (w@tson.dk)
+- anonymlol (anonymlol7@gmail.com)
+- Gediminas Petrikas (gedas18@gmail.com)
+- Adam Gotlib (gotlib.adam+dev@gmail.com)
+- Rémi Jouannet (remijouannet@gmail.com)
+- Andrea Tupini (tupini07@gmail.com)
#### Generated by bin/update-authors.sh.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5db417f9..3f23efd8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,17 +1,51 @@
# WebTorrent Desktop Version History
+## v0.10.0 - 2016-08-05
+
+### Added
+
+- Drag-and-drop magnet links (selected text) is now supported (#284)
+- Windows: Add "User Tasks" shortcuts to app icon in Start Menu (#114)
+- Linux: Show badge count for completed torrent downloads
+
+### Changed
+
+- Change WebTorrent Desktop peer ID prefix to 'WD' to distinguish from WebTorrent in the browser, 'WW' (#688)
+- Switch UI to React to improve UI rendering speed (#729)
+ - The primary bottleneck was actually `hyperx`, not `virtual-dom`.
+- Update Electron to 1.3.2 (#738) (#739) (#740) (#747) (#756)
+ - Mac 10.9: Fix the fullscreen button showing
+ - Mac 10.9: Fix window having border
+ - Mac 10.9: Fix occasional crash
+ - Mac: Update Squirrel.Mac to 0.2.1 (fixes situations in which updates would not get applied)
+ - Mac: Fix window not showing in Window menu
+ - Mac: Fix context menu always choosing first item by default
+ - Linux: Fix startup crashes (some Linux distros)
+ - Linux: Fix menubar not hiding after entering fullscreen (some Linux distros)
+- Improved location history (back/forward buttons) to fix rare exceptions (#687) (#748)
+ - Location history abstraction released independently as [`location-history`](https://www.npmjs.com/package/location-history)
+
+### Fixed
+
+- When streaming to VLC, set VLC window title to torrent file name (#746)
+- Fix "Cannot read property 'numPiecesPresent' of undefined" exception (#695)
+- Fix rare case where config file could not be completely written (#733)
+
## v0.9.0 - 2016-07-20
### Added
+
- Save selected subtitles
- Ask for confirmation before deleting torrents
- Support Debian Jessie
### Changed
+
- Only send telemetry in production
- Clean up the code. Split main.js, refactor lots of things
### Fixed
+
- Fix state.playing.jumpToTime behavior
- Remove torrent file and poster image when deleting a torrent
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a6899da9..048ab258 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -73,3 +73,23 @@ By making a contribution to this project, I certify that:
record of the contribution (including all personal information I submit with it,
including my sign-off) is maintained indefinitely and may be redistributed consistent
with this project or the open source license(s) involved.
+
+## Smoke Tests
+
+Before a release, check that the following basic use cases work correctly:
+
+1. Click "Play" to stream a built-in torrent (e.g. Sintel)
+ - Ensure that seeking to undownloaded region works and plays immediately.
+ - Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
+
+2. Check that the auto-updater works
+ - Open the console and check for the line "No update available" to indicate
+
+3. Add a new .torrent file via drag-and-drop.
+ - Ensure that it gets added to the list and starts downloading
+
+4. Remove a torrent from the client
+ - Ensure that the file is removed from `~/Downloads`
+
+5. Create and seed a new a torrent via drag-and-drop.
+ - Ensure that the torrent gets created and seeding begins.
diff --git a/bin/check-deps.js b/bin/check-deps.js
index f5e9e42c..f4a2dafc 100755
--- a/bin/check-deps.js
+++ b/bin/check-deps.js
@@ -45,7 +45,13 @@ var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
-var EXECUTABLE_DEPS = ['gh-release', 'standard', 'react-tools']
+var EXECUTABLE_DEPS = [
+ 'gh-release',
+ 'standard',
+ 'babel-cli',
+ 'babel-plugin-syntax-jsx',
+ 'babel-plugin-transform-react-jsx'
+]
main()
diff --git a/bin/package.js b/bin/package.js
index 4007bd91..9f72e190 100755
--- a/bin/package.js
+++ b/bin/package.js
@@ -19,6 +19,7 @@ var config = require('../src/config')
var pkg = require('../package.json')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
+var BUILD_PATH = path.join(config.ROOT_PATH, 'build')
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
var argv = minimist(process.argv.slice(2), {
@@ -36,6 +37,12 @@ var argv = minimist(process.argv.slice(2), {
function build () {
rimraf.sync(DIST_PATH)
+ rimraf.sync(BUILD_PATH)
+
+ console.log('Babel: Building JSX...')
+ cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
+ console.log('Babel: Built JSX.')
+
var platform = argv._[0]
if (platform === 'darwin') {
buildDarwin(printDone)
@@ -82,7 +89,7 @@ var all = {
// Pattern which specifies which files to ignore when copying files to create the
// package(s).
- ignore: /^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
+ ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
// The application name.
name: config.APP_NAME,
diff --git a/bin/update-authors.sh b/bin/update-authors.sh
index a6a6daac..4106eeb9 100755
--- a/bin/update-authors.sh
+++ b/bin/update-authors.sh
@@ -2,16 +2,16 @@
# Update AUTHORS.md based on git history.
-git log --reverse --format='%aN <%aE>' | perl -we '
+git log --reverse --format='%aN (%aE)' | perl -we '
BEGIN {
%seen = (), @authors = ();
}
while (<>) {
next if $seen{$_};
- next if //;
- next if //;
- next if //;
- next if //;
+ next if /(support\@greenkeeper.io)/;
+ next if /(ungoldman\@gmail.com)/;
+ next if /(dc\@DCs-MacBook.local)/;
+ next if /(rolandoguedes\@gmail.com)/;
$seen{$_} = push @authors, "- ", $_;
}
END {
diff --git a/package.json b/package.json
index d80c3115..a976c471 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
- "version": "0.9.0",
+ "version": "0.10.0",
"author": {
"name": "WebTorrent, LLC",
"email": "feross@webtorrent.io",
@@ -21,8 +21,8 @@
"create-torrent": "^3.24.5",
"deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0",
- "drag-drop": "^2.11.0",
- "electron-prebuilt": "1.3.2",
+ "drag-drop": "^2.12.1",
+ "electron-prebuilt": "1.3.3",
"fs-extra": "^0.30.0",
"hat": "0.0.3",
"iso-639-1": "^1.2.1",
@@ -45,6 +45,9 @@
"zero-fill": "^2.2.3"
},
"devDependencies": {
+ "babel-cli": "^6.11.4",
+ "babel-plugin-syntax-jsx": "^6.13.0",
+ "babel-plugin-transform-react-jsx": "^6.8.0",
"cross-zip": "^2.0.1",
"electron-osx-sign": "^0.3.0",
"electron-packager": "^7.0.0",
@@ -55,7 +58,6 @@
"nobin-debian-installer": "^0.0.10",
"open": "0.0.5",
"plist": "^1.2.0",
- "react-tools": "^0.13.3",
"rimraf": "^2.5.2",
"run-series": "^1.1.4",
"standard": "^7.0.0"
@@ -85,10 +87,12 @@
"url": "git://github.com/feross/webtorrent-desktop.git"
},
"scripts": {
+ "build": "babel --quiet src --out-dir build",
"clean": "node ./bin/clean.js",
"open-config": "node ./bin/open-config.js",
- "package": "rimraf build/ && jsx --es6module src/ build/ && node ./bin/package.js",
- "start": "jsx --es6module src/ build/ && electron .",
+ "package": "node ./bin/package.js",
+ "prepublish": "npm run build",
+ "start": "npm run build && electron .",
"test": "standard && node ./bin/check-deps.js",
"update-authors": "./bin/update-authors.sh"
}
diff --git a/src/.babelrc b/src/.babelrc
new file mode 100644
index 00000000..9d3c6b01
--- /dev/null
+++ b/src/.babelrc
@@ -0,0 +1,6 @@
+{
+ "plugins": [
+ "syntax-jsx",
+ "transform-react-jsx"
+ ]
+}
diff --git a/src/main/announcement.js b/src/main/announcement.js
index 275bc0de..9f7155d0 100644
--- a/src/main/announcement.js
+++ b/src/main/announcement.js
@@ -32,7 +32,7 @@ function init () {
function onResponse (err, res, data) {
if (err) return log(`Failed to retrieve announcement: ${err.message}`)
- if (res.statusCode !== 200) return log('No announcement exists')
+ if (res.statusCode !== 200) return log('No announcement available')
try {
data = JSON.parse(data.toString())
diff --git a/src/main/index.js b/src/main/index.js
index 20b5a3fe..b422a2dc 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -10,7 +10,6 @@ var config = require('../config')
var crashReporter = require('../crash-reporter')
var dialog = require('./dialog')
var dock = require('./dock')
-var handlers = require('./handlers')
var ipc = require('./ipc')
var log = require('./log')
var menu = require('./menu')
@@ -23,7 +22,11 @@ var windows = require('./windows')
var shouldQuit = false
var argv = sliceArgv(process.argv)
-if (!argv.includes('--dev')) process.env.NODE_ENV = 'production'
+if (config.IS_PRODUCTION) {
+ // When Electron is running in production mode (packaged app), then run React
+ // in production mode too.
+ process.env.NODE_ENV = 'production'
+}
if (process.platform === 'win32') {
shouldQuit = squirrelWin32.handleEvent(argv[0])
@@ -107,7 +110,6 @@ function init () {
function delayedInit () {
announcement.init()
dock.init()
- handlers.install()
tray.init()
updater.init()
userTasks.init()
diff --git a/src/main/ipc.js b/src/main/ipc.js
index eb4ba876..c5b5db87 100644
--- a/src/main/ipc.js
+++ b/src/main/ipc.js
@@ -8,6 +8,7 @@ var app = electron.app
var dialog = require('./dialog')
var dock = require('./dock')
+var handlers = require('./handlers')
var log = require('./log')
var menu = require('./menu')
var powerSaveBlocker = require('./power-save-blocker')
@@ -60,14 +61,14 @@ function init () {
*/
ipc.on('onPlayerOpen', function () {
- menu.onPlayerOpen()
+ menu.setPlayerOpen(true)
powerSaveBlocker.enable()
shortcuts.enable()
thumbar.enable()
})
ipc.on('onPlayerClose', function () {
- menu.onPlayerClose()
+ menu.setPlayerOpen(false)
powerSaveBlocker.disable()
shortcuts.disable()
thumbar.disable()
@@ -91,6 +92,14 @@ function init () {
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
+ /**
+ * File handlers
+ */
+ ipc.on('setDefaultFileHandler', (e, flag) => {
+ if (flag) handlers.install()
+ else handlers.uninstall()
+ })
+
/**
* Windows: Main
*/
@@ -103,6 +112,7 @@ function init () {
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
ipc.on('show', () => main.show())
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
+ ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
/**
* VLC
diff --git a/src/main/menu.js b/src/main/menu.js
index d68a6dfc..9bad2926 100644
--- a/src/main/menu.js
+++ b/src/main/menu.js
@@ -1,11 +1,10 @@
module.exports = {
init,
- onPlayerClose,
- onPlayerOpen,
+ setPlayerOpen,
+ setWindowFocus,
+ setAllowNav,
onToggleAlwaysOnTop,
- onToggleFullScreen,
- onWindowBlur,
- onWindowFocus
+ onToggleFullScreen
}
var electron = require('electron')
@@ -24,26 +23,28 @@ function init () {
electron.Menu.setApplicationMenu(menu)
}
-function onPlayerClose () {
- getMenuItem('Play/Pause').enabled = false
- getMenuItem('Increase Volume').enabled = false
- getMenuItem('Decrease Volume').enabled = false
- getMenuItem('Step Forward').enabled = false
- getMenuItem('Step Backward').enabled = false
- getMenuItem('Increase Speed').enabled = false
- getMenuItem('Decrease Speed').enabled = false
- getMenuItem('Add Subtitles File...').enabled = false
+function setPlayerOpen (flag) {
+ getMenuItem('Play/Pause').enabled = flag
+ getMenuItem('Increase Volume').enabled = flag
+ getMenuItem('Decrease Volume').enabled = flag
+ getMenuItem('Step Forward').enabled = flag
+ getMenuItem('Step Backward').enabled = flag
+ getMenuItem('Increase Speed').enabled = flag
+ getMenuItem('Decrease Speed').enabled = flag
+ getMenuItem('Add Subtitles File...').enabled = flag
}
-function onPlayerOpen () {
- getMenuItem('Play/Pause').enabled = true
- getMenuItem('Increase Volume').enabled = true
- getMenuItem('Decrease Volume').enabled = true
- getMenuItem('Step Forward').enabled = true
- getMenuItem('Step Backward').enabled = true
- getMenuItem('Increase Speed').enabled = true
- getMenuItem('Decrease Speed').enabled = true
- getMenuItem('Add Subtitles File...').enabled = true
+function setWindowFocus (flag) {
+ getMenuItem('Full Screen').enabled = flag
+ getMenuItem('Float on Top').enabled = flag
+}
+
+// Disallow opening more screens on top of the current one.
+function setAllowNav (flag) {
+ getMenuItem('Preferences').enabled = flag
+ getMenuItem('Create New Torrent...').enabled = flag
+ var item = getMenuItem('Create New Torrent from File...')
+ if (item) item.enabled = flag
}
function onToggleAlwaysOnTop (flag) {
@@ -54,16 +55,6 @@ function onToggleFullScreen (flag) {
getMenuItem('Full Screen').checked = flag
}
-function onWindowBlur () {
- getMenuItem('Full Screen').enabled = false
- getMenuItem('Float on Top').enabled = false
-}
-
-function onWindowFocus () {
- getMenuItem('Full Screen').enabled = true
- getMenuItem('Float on Top').enabled = true
-}
-
function getMenuItem (label) {
for (var i = 0; i < menu.items.length; i++) {
var menuItem = menu.items[i].submenu.items.find(function (item) {
@@ -130,14 +121,6 @@ function getMenuTemplate () {
},
{
role: 'selectall'
- },
- {
- type: 'separator'
- },
- {
- label: 'Preferences',
- accelerator: 'CmdOrCtrl+,',
- click: () => windows.main.dispatch('preferences')
}
]
},
@@ -350,6 +333,17 @@ function getMenuTemplate () {
click: () => dialog.openSeedFile()
})
+ // Edit menu (Windows, Linux)
+ template[1].submenu.push(
+ {
+ type: 'separator'
+ },
+ {
+ label: 'Preferences',
+ accelerator: 'CmdOrCtrl+,',
+ click: () => windows.main.dispatch('preferences')
+ })
+
// Help menu (Windows, Linux)
template[4].submenu.push(
{
diff --git a/src/main/tray.js b/src/main/tray.js
index 22a55b69..7816ff4b 100644
--- a/src/main/tray.js
+++ b/src/main/tray.js
@@ -1,8 +1,7 @@
module.exports = {
hasTray,
init,
- onWindowBlur,
- onWindowFocus
+ setWindowFocus
}
var electron = require('electron')
@@ -31,12 +30,7 @@ function hasTray () {
return !!tray
}
-function onWindowBlur () {
- if (!tray) return
- updateTrayMenu()
-}
-
-function onWindowFocus () {
+function setWindowFocus (flag) {
if (!tray) return
updateTrayMenu()
}
diff --git a/src/main/updater.js b/src/main/updater.js
index 9fe30eb6..d21ab4e2 100644
--- a/src/main/updater.js
+++ b/src/main/updater.js
@@ -63,7 +63,7 @@ function initDarwinWin32 () {
electron.autoUpdater.on(
'update-not-available',
- () => log('Update not available')
+ () => log('No update available')
)
electron.autoUpdater.on(
diff --git a/src/main/windows/main.js b/src/main/windows/main.js
index bad4b522..d60959b6 100644
--- a/src/main/windows/main.js
+++ b/src/main/windows/main.js
@@ -141,7 +141,9 @@ function setBounds (bounds, maximize) {
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
log('setBounds: centered to ' + JSON.stringify(bounds))
}
- main.win.setBounds(bounds, true)
+ // Resize the window's content area (so window border doesn't need to be taken
+ // into account)
+ main.win.setContentBounds(bounds, true)
} else {
log('setBounds: not setting bounds because of window maximization')
}
@@ -204,13 +206,13 @@ function toggleFullScreen (flag) {
}
function onWindowBlur () {
- menu.onWindowBlur()
- tray.onWindowBlur()
+ menu.setWindowFocus(false)
+ tray.setWindowFocus(false)
}
function onWindowFocus () {
- menu.onWindowFocus()
- tray.onWindowFocus()
+ menu.setWindowFocus(true)
+ tray.setWindowFocus(true)
}
function getIconPath () {
diff --git a/src/renderer/controllers/playback-controller.js b/src/renderer/controllers/playback-controller.js
index c80489af..8df16af9 100644
--- a/src/renderer/controllers/playback-controller.js
+++ b/src/renderer/controllers/playback-controller.js
@@ -188,7 +188,7 @@ module.exports = class PlaybackController {
}, 10000) /* give it a few seconds */
if (torrentSummary.status === 'paused') {
- dispatch('startTorrentingSummary', torrentSummary)
+ dispatch('startTorrentingSummary', torrentSummary.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
} else {
@@ -242,7 +242,7 @@ module.exports = class PlaybackController {
}
// otherwise, play the video
- dispatch('setTitle', torrentSummary.files[state.playing.fileIndex].name)
+ state.window.title = torrentSummary.files[state.playing.fileIndex].name
this.update()
ipcRenderer.send('onPlayerOpen')
diff --git a/src/renderer/controllers/prefs-controller.js b/src/renderer/controllers/prefs-controller.js
index 9ba400bf..97171c8a 100644
--- a/src/renderer/controllers/prefs-controller.js
+++ b/src/renderer/controllers/prefs-controller.js
@@ -1,5 +1,6 @@
-const {dispatch} = require('../lib/dispatcher')
const State = require('../lib/state')
+const {dispatch} = require('../lib/dispatcher')
+const ipcRenderer = require('electron').ipcRenderer
// Controls the Preferences screen
module.exports = class PrefsController {
@@ -15,11 +16,15 @@ module.exports = class PrefsController {
url: 'preferences',
setup: function (cb) {
// initialize preferences
- dispatch('setTitle', 'Preferences')
+ state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
+ ipcRenderer.send('setAllowNav', false)
cb()
},
- destroy: () => this.save()
+ destroy: () => {
+ ipcRenderer.send('setAllowNav', true)
+ this.save()
+ }
})
}
@@ -41,7 +46,11 @@ module.exports = class PrefsController {
// All unsaved prefs take effect atomically, and are saved to config.json
save () {
var state = this.state
+ if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
+ ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
+ }
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
State.save(state)
+ dispatch('checkDownloadPath')
}
}
diff --git a/src/renderer/controllers/torrent-list-controller.js b/src/renderer/controllers/torrent-list-controller.js
index c259184d..5ad278e2 100644
--- a/src/renderer/controllers/torrent-list-controller.js
+++ b/src/renderer/controllers/torrent-list-controller.js
@@ -24,8 +24,8 @@ module.exports = class TorrentListController {
// Use path string instead of W3C File object
torrentId = torrentId.path
}
+
// Allow a instant.io link to be pasted
- // TODO: remove this once support is added to webtorrent core
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
}
@@ -40,12 +40,21 @@ module.exports = class TorrentListController {
// Shows the Create Torrent page with options to seed a given file or folder
showCreateTorrent (files) {
+ // You can only create torrents from the home screen.
+ if (this.state.location.url() !== 'home') {
+ return dispatch('error', 'Please go back to the torrent list before creating a new torrent.')
+ }
+
// Files will either be an array of file objects, which we can send directly
// to the create-torrent screen
if (files.length === 0 || typeof files[0] !== 'string') {
this.state.location.go({
url: 'create-torrent',
- files: files
+ files: files,
+ setup: (cb) => {
+ this.state.window.title = 'Create New Torrent'
+ cb(null)
+ }
})
return
}
@@ -67,27 +76,29 @@ module.exports = class TorrentListController {
var state = this.state
var torrentKey = state.nextTorrentKey++
ipcRenderer.send('wt-create-torrent', torrentKey, options)
- state.location.backToFirst(function () {
- state.location.clearForward('create-torrent')
- })
+ state.location.cancel()
}
// Starts downloading and/or seeding a given torrentSummary.
- startTorrentingSummary (torrentSummary) {
- var s = torrentSummary
-
- // Backward compatibility for config files save before we had torrentKey
- if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++
+ startTorrentingSummary (torrentKey) {
+ var s = TorrentSummary.getByKey(this.state, torrentKey)
+ if (!s) throw new Error('Missing key: ' + torrentKey)
// Use Downloads folder by default
if (!s.path) s.path = this.state.saved.prefs.downloadPath
- ipcRenderer.send('wt-start-torrenting',
- s.torrentKey,
- TorrentSummary.getTorrentID(s),
- s.path,
- s.fileModtimes,
- s.selections)
+ fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
+ if (err) {
+ s.error = 'path-missing'
+ return
+ }
+ ipcRenderer.send('wt-start-torrenting',
+ s.torrentKey,
+ TorrentSummary.getTorrentID(s),
+ s.path,
+ s.fileModtimes,
+ s.selections)
+ })
}
// TODO: use torrentKey, not infoHash
@@ -95,7 +106,7 @@ module.exports = class TorrentListController {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new'
- this.startTorrentingSummary(torrentSummary)
+ this.startTorrentingSummary(torrentSummary.torrentKey)
sound.play('ENABLE')
} else {
torrentSummary.status = 'paused'
@@ -271,6 +282,7 @@ function saveTorrentFileAs (torrentSummary) {
]
}
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
+ if (!savePath) return // They clicked Cancel
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
diff --git a/src/renderer/lib/migrations.js b/src/renderer/lib/migrations.js
index 7c1aa8a6..b284f4c6 100644
--- a/src/renderer/lib/migrations.js
+++ b/src/renderer/lib/migrations.js
@@ -25,6 +25,10 @@ function run (state) {
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
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
+ }
+}
diff --git a/src/renderer/lib/state.js b/src/renderer/lib/state.js
index 3205e1df..04350337 100644
--- a/src/renderer/lib/state.js
+++ b/src/renderer/lib/state.js
@@ -200,6 +200,9 @@ function save (state, cb) {
if (key === 'playStatus') {
continue // Don't save whether a torrent is playing / pending
}
+ if (key === 'error') {
+ continue // Don't save error states
+ }
torrent[key] = x[key]
}
return torrent
diff --git a/src/renderer/main.js b/src/renderer/main.js
index b131d997..15fd2183 100644
--- a/src/renderer/main.js
+++ b/src/renderer/main.js
@@ -7,6 +7,7 @@ const dragDrop = require('drag-drop')
const electron = require('electron')
const React = require('react')
const ReactDOM = require('react-dom')
+const fs = require('fs')
const config = require('../config')
const App = require('./views/app')
@@ -77,21 +78,27 @@ function onState (err, _state) {
// Restart everything we were torrenting last time the app ran
resumeTorrents()
- // Lazy-load other stuff, like the AppleTV module, later to keep startup fast
- window.setTimeout(delayedInit, config.DELAYED_INIT)
-
- // Listen for messages from the main process
- setupIpc()
-
// Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary
// a progress bar and to keep the cursor in sync when playing a video
setInterval(update, 1000)
app = ReactDOM.render( , document.querySelector('#body'))
+ // Lazy-load other stuff, like the AppleTV module, later to keep startup fast
+ window.setTimeout(delayedInit, config.DELAYED_INIT)
+
+ // Listen for messages from the main process
+ setupIpc()
+
+ // Warn if the download dir is gone, eg b/c an external drive is unplugged
+ checkDownloadPath()
+
// OS integrations:
- // ...drag and drop a torrent or video file to play or seed
- dragDrop('body', onOpen)
+ // ...drag and drop files/text to start torrenting or seeding
+ dragDrop('body', {
+ onDrop: onOpen,
+ onDropText: onOpen
+ })
// ...same thing if you paste a torrent
document.addEventListener('paste', onPaste)
@@ -172,8 +179,7 @@ const dispatchHandlers = {
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
- 'startTorrentingSummary': (torrentSummary) =>
- controllers.torrentList.startTorrentingSummary(torrentSummary),
+ 'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
// Playback
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
@@ -209,6 +215,7 @@ const dispatchHandlers = {
// Preferences screen
'preferences': () => controllers.prefs.show(),
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
+ 'checkDownloadPath': checkDownloadPath,
// Update (check for new versions on Linux, where there's no auto updater)
'updateAvailable': (version) => controllers.update.updateAvailable(version),
@@ -220,6 +227,7 @@ const dispatchHandlers = {
'escapeBack': escapeBack,
'back': () => state.location.back(),
'forward': () => state.location.forward(),
+ 'cancel': () => state.location.cancel(),
// Controlling the window
'setDimensions': setDimensions,
@@ -309,8 +317,14 @@ function escapeBack () {
// Starts all torrents that aren't paused on program startup
function resumeTorrents () {
state.saved.torrents
- .filter((torrentSummary) => torrentSummary.status !== 'paused')
- .forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary))
+ .map((torrentSummary) => {
+ // Torrent keys are ephemeral, reassigned each time the app runs.
+ // On startup, give all torrents a key, even the ones that are paused.
+ torrentSummary.torrentKey = state.nextTorrentKey++
+ return torrentSummary
+ })
+ .filter((s) => s.status !== 'paused')
+ .forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey))
}
// Set window dimensions to match video dimensions or fill the screen
@@ -357,25 +371,25 @@ function setDimensions (dimensions) {
function onOpen (files) {
if (!Array.isArray(files)) files = [ files ]
- if (state.modal) {
+ var url = state.location.url()
+ var allTorrents = files.every(TorrentPlayer.isTorrent)
+ var allSubtitles = files.every(controllers.subtitles.isSubtitle)
+
+ if (allTorrents) {
+ // Drop torrents onto the app: go to home screen, add torrents, no matter what
+ dispatch('backToList')
+ // All .torrent files? Add them.
+ files.forEach((file) => controllers.torrentList.addTorrent(file))
+ } else if (url === 'player' && allSubtitles) {
+ // Drop subtitles onto a playing video: add subtitles
+ controllers.subtitles.addSubtitles(files, true)
+ } else if (url === 'home') {
+ // Drop files onto home screen: show Create Torrent
state.modal = null
- }
-
- var subtitles = files.filter(controllers.subtitles.isSubtitle)
-
- if (state.location.url() === 'home' || subtitles.length === 0) {
- if (files.every(TorrentPlayer.isTorrent)) {
- if (state.location.url() !== 'home') {
- dispatch('backToList')
- }
- // All .torrent files? Add them.
- files.forEach((file) => controllers.torrentList.addTorrent(file))
- } else {
- // Show the Create Torrent screen. Let's seed those files.
- controllers.torrentList.showCreateTorrent(files)
- }
- } else if (state.location.url() === 'player') {
- controllers.subtitles.addSubtitles(subtitles, true)
+ controllers.torrentList.showCreateTorrent(files)
+ } else {
+ // Drop files onto any other screen: show error
+ return onError('Please go back to the torrent list before creating a new torrent.')
}
update()
@@ -429,3 +443,14 @@ function onFullscreenChanged (e, isFullScreen) {
update()
}
+
+function checkDownloadPath () {
+ fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
+ if (err) {
+ state.downloadPathStatus = 'missing'
+ return console.error(err)
+ }
+ if (stat.isDirectory()) state.downloadPathStatus = 'ok'
+ else state.downloadPathStatus = 'missing'
+ })
+}
diff --git a/src/renderer/views/create-torrent-error-page.js b/src/renderer/views/create-torrent-error-page.js
index b59a360f..3eff6db2 100644
--- a/src/renderer/views/create-torrent-error-page.js
+++ b/src/renderer/views/create-torrent-error-page.js
@@ -16,7 +16,7 @@ module.exports = class CreateTorrentErrorPage extends React.Component {
-
+
Cancel
diff --git a/src/renderer/views/create-torrent.js b/src/renderer/views/create-torrent.js
index 7fe4a308..938b12ed 100644
--- a/src/renderer/views/create-torrent.js
+++ b/src/renderer/views/create-torrent.js
@@ -77,7 +77,7 @@ module.exports = class CreateTorrentPage extends React.Component {
Trackers:
-
+
Private:
@@ -89,7 +89,7 @@ module.exports = class CreateTorrentPage extends React.Component {
- Cancel
+ Cancel
Create Torrent
diff --git a/src/renderer/views/preferences.js b/src/renderer/views/preferences.js
index 3caaac93..c4d2feba 100644
--- a/src/renderer/views/preferences.js
+++ b/src/renderer/views/preferences.js
@@ -22,11 +22,12 @@ function renderGeneralSection (state) {
description: '',
icon: 'settings'
}, [
- renderDownloadDirSelector(state)
+ renderDownloadPathSelector(state),
+ renderFileHandlers(state)
])
}
-function renderDownloadDirSelector (state) {
+function renderDownloadPathSelector (state) {
return renderFileSelector({
key: 'download-path',
label: 'Download Path',
@@ -39,10 +40,33 @@ function renderDownloadDirSelector (state) {
},
state.unsaved.prefs.downloadPath,
function (filePath) {
- setStateValue('downloadPath', filePath)
+ dispatch('updatePreferences', 'downloadPath', filePath)
})
}
+function renderFileHandlers (state) {
+ var definition = {
+ key: 'file-handlers',
+ label: 'Handle Torrent Files'
+ }
+ var buttonText = state.unsaved.prefs.isFileHandler
+ ? 'Remove default app for torrent files'
+ : 'Make WebTorrent the default app for torrent files'
+ var controls = [(
+
+ {buttonText}
+
+ )]
+ return renderControlGroup(definition, controls)
+
+ function toggleFileHandlers () {
+ var isFileHandler = state.unsaved.prefs.isFileHandler
+ dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
+ }
+}
+
// Renders a prefs section.
// - definition should be {icon, title, description}
// - controls should be an array of vdom elements
@@ -73,25 +97,24 @@ function renderSection (definition, controls) {
// - value should be the current pref, a file or folder path
// - callback takes a new file or folder path
function renderFileSelector (definition, value, callback) {
- return (
-
-
-
- {definition.label}
- {definition.description}
-
-
-
-
- folder_open
-
-
-
-
- )
+ var controls = [(
+
+ ), (
+
+ folder_open
+
+ )]
+ return renderControlGroup(definition, controls)
+
function handleClick () {
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
if (!Array.isArray(filenames)) return
@@ -100,6 +123,18 @@ function renderFileSelector (definition, value, callback) {
}
}
-function setStateValue (property, value) {
- dispatch('updatePreferences', property, value)
+function renderControlGroup (definition, controls) {
+ return (
+
+
+
+ {definition.label}
+ {definition.description}
+
+
+ {controls}
+
+
+
+ )
}
diff --git a/src/renderer/views/torrent-list.js b/src/renderer/views/torrent-list.js
index 74afa8cf..50cf8bfe 100644
--- a/src/renderer/views/torrent-list.js
+++ b/src/renderer/views/torrent-list.js
@@ -8,16 +8,32 @@ const {dispatcher} = require('../lib/dispatcher')
module.exports = class TorrentList extends React.Component {
render () {
var state = this.props.state
- var torrentRows = state.saved.torrents.map(
+
+ var contents = []
+ if (state.downloadPathStatus === 'missing') {
+ contents.push(
+
+
Download path missing: {state.saved.prefs.downloadPath}
+
Check that all drives are connected?
+
Alternatively, choose a new download path
+ in Preferences
+
+
+ )
+ }
+ var torrentElems = state.saved.torrents.map(
(torrentSummary) => this.renderTorrent(torrentSummary)
)
+ contents.push(...torrentElems)
+ contents.push(
+
+ Drop a torrent file here or paste a magnet link
+
+ )
return (
- {torrentRows}
-
- Drop a torrent file here or paste a magnet link
-
+ {contents}
)
}
@@ -44,6 +60,7 @@ module.exports = class TorrentList extends React.Component {
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled')
+ if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
return (
+ {getErrorMessage(torrentSummary)}
+
+ )
+ } else if (torrentSummary.status !== 'paused' && prog) {
+ elements.push(
{renderPercentProgress()}
{renderTotalProgress()}
@@ -77,7 +100,7 @@ module.exports = class TorrentList extends React.Component {
{renderUploadSpeed()}
{renderEta()}
- ))
+ )
}
return ({elements}
)
@@ -174,8 +197,9 @@ module.exports = class TorrentList extends React.Component {
}
// Only show the play button for torrents that contain playable media
- var playButton
- if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
+ var playButton, downloadButton
+ var noErrors = !torrentSummary.error
+ if (noErrors && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
playButton = (
)
}
-
- return (
-
- {positionElem}
- {playButton}
+ if (noErrors) {
+ downloadButton = (
{downloadIcon}
+ )
+ }
+
+ return (
+
+ {positionElem}
+ {playButton}
+ {downloadButton}
{message}
)
+ if (torrentSummary.error || !torrentSummary.files) {
+ var message = ''
+ if (torrentSummary.error === 'path-missing') {
+ // Special case error: this torrent's download dir or file is missing
+ message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
+ } else if (torrentSummary.error) {
+ // General error for this torrent: just show the message
+ message = torrentSummary.error.message || torrentSummary.error
+ } else if (torrentSummary.status === 'paused') {
+ // No file info, no infohash, and we're not trying to download from the DHT
+ message = 'Failed to load torrent info. Click the download button to try again...'
+ } else {
+ // No file info, no infohash, trying to load from the DHT
+ message = 'Downloading torrent info...'
+ }
+ filesElement = (
+
+ {message}
+
+ )
} else {
// We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files
@@ -255,7 +298,8 @@ module.exports = class TorrentList extends React.Component {
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
var isDone = false // Are we finished torrenting it?
var progress = ''
- if (torrentSummary.progress && torrentSummary.progress.files) {
+ if (torrentSummary.progress && torrentSummary.progress.files &&
+ torrentSummary.progress.files[index]) {
var fileProg = torrentSummary.progress.files[index]
isDone = fileProg.numPiecesPresent === fileProg.numPieces
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
@@ -327,3 +371,16 @@ module.exports = class TorrentList extends React.Component {
)
}
}
+
+function getErrorMessage (torrentSummary) {
+ var err = torrentSummary.error
+ if (err === 'path-missing') {
+ return (
+
+ Path missing.
+ Fix and restart the app, or delete the torrent.
+
+ )
+ }
+ return 'Error'
+}
diff --git a/src/renderer/views/update-available-modal.js b/src/renderer/views/update-available-modal.js
index 885ba253..49b8759b 100644
--- a/src/renderer/views/update-available-modal.js
+++ b/src/renderer/views/update-available-modal.js
@@ -11,18 +11,18 @@ module.exports = class UpdateAvailableModal extends React.Component {
A new version of WebTorrent is available: v{state.modal.version}
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.
- Skip This Release
- Show Download Page
+ Skip This Release
+ Show Download Page
)
- function handleOK () {
+ function handleShow () {
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
dispatch('exitModal')
}
- function handleCancel () {
+ function handleSkip () {
dispatch('skipVersion', state.modal.version)
dispatch('exitModal')
}
diff --git a/static/main.css b/static/main.css
index d56c9838..a2b1a8b7 100644
--- a/static/main.css
+++ b/static/main.css
@@ -551,6 +551,19 @@ input[type='text'] {
line-height: 1.5em;
}
+/*
+ * TORRENT LIST: ERRORS
+ */
+
+.torrent-list p {
+ margin: 10px 20px;
+}
+
+.torrent-list a {
+ color: #99f;
+ text-decoration: none;
+}
+
/*
* TORRENT LIST: DRAG-DROP TARGET
*/