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 20f037ee..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|minimist|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 492e1a2b..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.1", + "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": "rm -rf 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 c81517e1..b422a2dc 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -10,19 +10,23 @@ 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') var squirrelWin32 = require('./squirrel-win32') var tray = require('./tray') var updater = require('./updater') +var userTasks = require('./user-tasks') 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]) @@ -106,9 +110,9 @@ function init () { function delayedInit () { announcement.init() dock.init() - handlers.install() tray.init() updater.init() + userTasks.init() } function onOpen (e, torrentId) { 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/user-tasks.js b/src/main/user-tasks.js new file mode 100644 index 00000000..47e63f8a --- /dev/null +++ b/src/main/user-tasks.js @@ -0,0 +1,43 @@ +module.exports = { + init +} + +var electron = require('electron') + +var app = electron.app + +/** + * Add a user task menu to the app icon on right-click. (Windows) + */ +function init () { + if (process.platform !== 'win32') return + app.setUserTasks(getUserTasks()) +} + +function getUserTasks () { + return [ + { + arguments: '-n', + title: 'Create New Torrent...', + description: 'Create a new torrent' + }, + { + arguments: '-o', + title: 'Open Torrent File...', + description: 'Open a .torrent file' + }, + { + arguments: '-u', + title: 'Open Torrent Address...', + description: 'Open a torrent from a URL' + } + ].map(getUserTasksItem) +} + +function getUserTasksItem (item) { + return Object.assign(item, { + program: process.execPath, + iconPath: process.execPath, + iconIndex: 0 + }) +} 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 3476812e..397b0e12 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 { @@ -250,7 +250,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 {

-

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 {
- +
@@ -89,7 +89,7 @@ module.exports = class CreateTorrentPage extends React.Component {
- +
diff --git a/src/renderer/views/preferences.js b/src/renderer/views/preferences.js index c69566df..26c41e97 100644 --- a/src/renderer/views/preferences.js +++ b/src/renderer/views/preferences.js @@ -23,7 +23,8 @@ function renderGeneralSection (state) { description: '', icon: 'settings' }, [ - renderDownloadDirSelector(state) + renderDownloadPathSelector(state), + renderFileHandlers(state) ]) } @@ -50,7 +51,7 @@ function renderPlayInVlcSelector (state) { }) } -function renderDownloadDirSelector (state) { +function renderDownloadPathSelector (state) { return renderFileSelector({ key: 'download-path', label: 'Download Path', @@ -63,10 +64,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 = [( + + )] + 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 @@ -126,25 +150,24 @@ function renderCheckbox (definition, value, callback) { // - 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 ( -
-
- -
- - -
-
-
- ) + var controls = [( + + ), ( + + )] + return renderControlGroup(definition, controls) + function handleClick () { dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) { if (!Array.isArray(filenames)) return @@ -153,6 +176,18 @@ function renderFileSelector (definition, value, callback) { } } -function setStateValue (property, value) { - dispatch('updatePreferences', property, value) +function renderControlGroup (definition, controls) { + return ( +
+
+ +
+ {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.

- - + +

) - 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 4907765d..e42dd878 100644 --- a/static/main.css +++ b/static/main.css @@ -555,6 +555,19 @@ input[type='text'] { line-height: 1.5em; } +/* + * TORRENT LIST: ERRORS + */ + +.torrent-list p { + margin: 10px 20px; +} + +.torrent-list a { + color: #99f; + text-decoration: none; +} + /* * TORRENT LIST: DRAG-DROP TARGET */