diff --git a/renderer/controllers/torrent-list-controller.js b/renderer/controllers/torrent-list-controller.js new file mode 100644 index 00000000..78a99841 --- /dev/null +++ b/renderer/controllers/torrent-list-controller.js @@ -0,0 +1,241 @@ +const fs = require('fs') +const path = require('path') +const electron = require('electron') + +const {dispatch} = require('../lib/dispatcher') +const State = require('../lib/state') +const sound = require('../lib/sound') +const TorrentSummary = require('../lib/torrent-summary') + +const ipcRenderer = electron.ipcRenderer + +const instantIoRegex = /^(https:\/\/)?instant\.io\/#/ + +// Controls the torrent list: creating, adding, deleting, & manipulating torrents +module.exports = class TorrentListController { + constructor (state) { + this.state = state + } + + // Adds a torrent to the list, starts downloading/seeding. TorrentID can be a + // magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent- + addTorrent (torrentId) { + if (torrentId.path) { + // 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) + } + + var torrentKey = this.state.nextTorrentKey++ + var path = this.state.saved.prefs.downloadPath + + ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path) + + dispatch('backToList') + } + + // Shows the Create Torrent page with options to seed a given file or folder + showCreateTorrent (files) { + // 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 + }) + return + } + + // ... or it will be an array of mixed file and folder paths. We have to walk + // through all the folders and find the files + findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles)) + } + + // Creates a new torrent and start seeeding + createTorrent (options) { + var state = this.state + var torrentKey = state.nextTorrentKey++ + ipcRenderer.send('wt-create-torrent', torrentKey, options) + state.location.backToFirst(function () { + state.location.clearForward('create-torrent') + }) + } + + // TODO: use torrentKey, not infoHash + toggleTorrent (infoHash) { + var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) + if (torrentSummary.status === 'paused') { + torrentSummary.status = 'new' + ipcRenderer.send('wt-start-torrenting', torrentSummary) + sound.play('ENABLE') + } else { + torrentSummary.status = 'paused' + ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash) + sound.play('DISABLE') + } + } + + toggleTorrentFile (infoHash, index) { + var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) + torrentSummary.selections[index] = !torrentSummary.selections[index] + + // Let the WebTorrent process know to start or stop fetching that file + ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections) + } + + // TODO: use torrentKey, not infoHash + deleteTorrent (infoHash, deleteData) { + var state = this.state + + ipcRenderer.send('wt-stop-torrenting', infoHash) + + if (deleteData) { + var torrentSummary = TorrentSummary.getByKey(state, infoHash) + moveItemToTrash(torrentSummary) + } + + var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash) + if (index > -1) state.saved.torrents.splice(index, 1) + State.saveThrottled(state) + state.location.clearForward('player') // prevent user from going forward to a deleted torrent + sound.play('DELETE') + } + + toggleSelectTorrent (infoHash) { + if (this.state.selectedInfoHash === infoHash) { + this.state.selectedInfoHash = null + } else { + this.state.selectedInfoHash = infoHash + } + } + + openTorrentContextMenu (infoHash) { + var torrentSummary = TorrentSummary.getByKey(this.state, infoHash) + var menu = new electron.remote.Menu() + + menu.append(new electron.remote.MenuItem({ + label: 'Remove From List', + click: () => this.deleteTorrent( + torrentSummary.infoHash, false) + })) + + menu.append(new electron.remote.MenuItem({ + label: 'Remove Data File', + click: () => this.deleteTorrent( + torrentSummary.infoHash, true) + })) + + menu.append(new electron.remote.MenuItem({ + type: 'separator' + })) + + if (torrentSummary.files) { + menu.append(new electron.remote.MenuItem({ + label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder', + click: () => showItemInFolder(torrentSummary) + })) + menu.append(new electron.remote.MenuItem({ + type: 'separator' + })) + } + + menu.append(new electron.remote.MenuItem({ + label: 'Copy Magnet Link to Clipboard', + click: () => electron.clipboard.writeText(torrentSummary.magnetURI) + })) + + menu.append(new electron.remote.MenuItem({ + label: 'Copy Instant.io Link to Clipboard', + click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`) + })) + + menu.append(new electron.remote.MenuItem({ + label: 'Save Torrent File As...', + click: () => saveTorrentFileAs(torrentSummary) + })) + + menu.popup(electron.remote.getCurrentWindow()) + } +} + +// Recursively finds {name, path, size} for all files in a folder +// Calls `cb` on success, calls `onError` on failure +function findFilesRecursive (paths, cb) { + if (paths.length > 1) { + var numComplete = 0 + var ret = [] + paths.forEach(function (path) { + findFilesRecursive([path], function (fileObjs) { + ret = ret.concat(fileObjs) + if (++numComplete === paths.length) { + ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path) + cb(ret) + } + }) + }) + return + } + + var fileOrFolder = paths[0] + fs.stat(fileOrFolder, function (err, stat) { + if (err) return dispatch('onError', err) + + // Files: return name, path, and size + if (!stat.isDirectory()) { + var filePath = fileOrFolder + return cb([{ + name: path.basename(filePath), + path: filePath, + size: stat.size + }]) + } + + // Folders: recurse, make a list of all the files + var folderPath = fileOrFolder + fs.readdir(folderPath, function (err, fileNames) { + if (err) return dispatch('onError', err) + var paths = fileNames.map((fileName) => path.join(folderPath, fileName)) + findFilesRecursive(paths, cb) + }) + }) +} + +// Delete all files in a torren +function moveItemToTrash (torrentSummary) { + // TODO: delete directories, not just files + torrentSummary.files.forEach(function (file) { + var filePath = path.join(torrentSummary.path, file.path) + console.log('DEBUG DELETING ' + filePath) + ipcRenderer.send('moveItemToTrash', filePath) + }) +} + +function showItemInFolder (torrentSummary) { + ipcRenderer.send('showItemInFolder', TorrentSummary.getTorrentPath(torrentSummary)) +} + +function saveTorrentFileAs (torrentSummary) { + var downloadPath = this.state.saved.prefs.downloadPath + var newFileName = `${path.parse(torrentSummary.name).name}.torrent` + var opts = { + title: 'Save Torrent File', + defaultPath: path.join(downloadPath, newFileName), + filters: [ + { name: 'Torrent Files', extensions: ['torrent'] }, + { name: 'All Files', extensions: ['*'] } + ] + } + electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) { + var torrentPath = TorrentSummary.getTorrentPath(torrentSummary) + fs.readFile(torrentPath, function (err, torrentFile) { + if (err) return dispatch('onError', err) + fs.writeFile(savePath, torrentFile, function (err) { + if (err) return dispatch('onError', err) + }) + }) + }) +} diff --git a/renderer/lib/telemetry.js b/renderer/lib/telemetry.js index 5c4b6249..82a3e555 100644 --- a/renderer/lib/telemetry.js +++ b/renderer/lib/telemetry.js @@ -116,6 +116,12 @@ function getApproxNumTorrents (state) { // An uncaught error happened in the main process or in one of the windows function logUncaughtError (procName, err) { + console.error('uncaught error', procName, err) + + // Not initialized yet? Ignore. + // Hopefully uncaught errors immediately on startup are fixed in dev + if (!telemetry) return + var message, stack if (typeof err === 'string') { message = err diff --git a/renderer/lib/torrent-summary.js b/renderer/lib/torrent-summary.js index 9587777c..56e048ff 100644 --- a/renderer/lib/torrent-summary.js +++ b/renderer/lib/torrent-summary.js @@ -1,6 +1,8 @@ module.exports = { getPosterPath, - getTorrentPath + getTorrentPath, + getByKey, + getTorrentID } var path = require('path') @@ -22,3 +24,22 @@ function getPosterPath (torrentSummary) { // Backslashes in URLS in CSS cause bizarre string encoding issues return posterPath.replace(/\\/g, '/') } + +// Expects a torrentSummary +// Returns a torrentID: filename, magnet URI, or infohash +function getTorrentID (torrentSummary) { + var s = torrentSummary + if (s.torrentFileName) { // Load torrent file from disk + return getTorrentPath(s) + } else { // Load torrent from DHT + return s.magnetURI || s.infoHash + } +} + +// Expects a torrentKey or infoHash +// Returns the corresponding torrentSummary, or undefined +function getByKey (state, torrentKey) { + if (!torrentKey) return undefined + return state.saved.torrents.find((x) => + x.torrentKey === torrentKey || x.infoHash === torrentKey) +} diff --git a/renderer/main.js b/renderer/main.js index 2f5e9bbc..a80b5a39 100644 --- a/renderer/main.js +++ b/renderer/main.js @@ -26,6 +26,7 @@ const TorrentSummary = require('./lib/torrent-summary') const MediaController = require('./controllers/media-controller') const UpdateController = require('./controllers/update-controller') const PrefsController = require('./controllers/prefs-controller') +const TorrentListController = require('./controllers/torrent-list-controller') // Yo-yo pattern: state object lives here and percolates down thru all the views. // Events come back up from the views via dispatch(...) @@ -59,7 +60,8 @@ function onState (err, _state) { controllers = { media: new MediaController(state), update: new UpdateController(state), - prefs: new PrefsController(state, config) + prefs: new PrefsController(state, config), + torrentList: new TorrentListController(state) } // Add first page to location history @@ -170,68 +172,44 @@ function dispatch (action, ...args) { console.log('dispatch: %s %o', action, args) } - if (action === 'onOpen') { - onOpen(args[0] /* files */) - } - if (action === 'addTorrent') { - addTorrent(args[0] /* torrent */) - } + // Torrent list: creating, deleting, selecting torrents if (action === 'openTorrentFile') { ipcRenderer.send('openTorrentFile') /* open torrent file */ } if (action === 'openFiles') { ipcRenderer.send('openFiles') /* add files with dialog */ } - if (action === 'showCreateTorrent') { - showCreateTorrent(args[0] /* paths */) - } if (action === 'openTorrentAddress') { state.modal = { id: 'open-torrent-address-modal' } } - if (action === 'createTorrent') { - createTorrent(args[0] /* options */) + if (action === 'addTorrent') { + controllers.torrentList.addTorrent(args[0] /* torrent */) } - if (action === 'openItem') { - openItem(args[0] /* infoHash */, args[1] /* index */) + if (action === 'showCreateTorrent') { + controllers.torrentList.showCreateTorrent(args[0] /* paths */) + } + if (action === 'createTorrent') { + controllers.torrentList.createTorrent(args[0] /* options */) } if (action === 'toggleTorrent') { - toggleTorrent(args[0] /* infoHash */) - } - if (action === 'deleteTorrent') { - deleteTorrent(args[0] /* infoHash */) - } - if (action === 'toggleSelectTorrent') { - toggleSelectTorrent(args[0] /* infoHash */) + controllers.torrentList.toggleTorrent(args[0] /* infoHash */) } if (action === 'toggleTorrentFile') { - toggleTorrentFile(args[0] /* infoHash */, args[1] /* index */) + controllers.torrentList.toggleTorrentFile(args[0] /* infoHash */, args[1] /* index */) + } + if (action === 'deleteTorrent') { + controllers.torrentList.deleteTorrent(args[0] /* infoHash */) + } + if (action === 'toggleSelectTorrent') { + controllers.torrentList.toggleSelectTorrent(args[0] /* infoHash */) } if (action === 'openTorrentContextMenu') { - openTorrentContextMenu(args[0] /* infoHash */) + controllers.torrentList.openTorrentContextMenu(args[0] /* infoHash */) } - if (action === 'toggleCastMenu') { - lazyLoadCast().toggleMenu(args[0] /* deviceType */) - } - if (action === 'selectCastDevice') { - lazyLoadCast().selectDevice(args[0] /* index */) - } - if (action === 'stopCasting') { - lazyLoadCast().stop() - } - if (action === 'setDimensions') { - setDimensions(args[0] /* dimensions */) - } - if (action === 'backToList') { - backToList() - } - if (action === 'escapeBack') { - escapeBack() - } - if (action === 'back') { - state.location.back() - } - if (action === 'forward') { - state.location.forward() + + // Playback + if (action === 'openItem') { + openItem(args[0] /* infoHash */, args[1] /* index */) } if (action === 'playPause') { playPause() @@ -284,30 +262,72 @@ function dispatch (action, ...args) { if (action === 'vlcNotFound') { controllers.media.vlcNotFound() } - if (action === 'toggleFullScreen') { - ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */) + + // Casting: Chromecast, Airplay, etc + if (action === 'toggleCastMenu') { + lazyLoadCast().toggleMenu(args[0] /* deviceType */) } - if (action === 'exitModal') { - state.modal = null + if (action === 'selectCastDevice') { + lazyLoadCast().selectDevice(args[0] /* index */) } + if (action === 'stopCasting') { + lazyLoadCast().stop() + } + + // Preferences screen if (action === 'preferences') { controllers.prefs.show() } if (action === 'updatePreferences') { controllers.prefs.update(args[0] /* key */, args[1] /* value */) } + + // Update (check for new versions on Linux, where there's no auto updater) if (action === 'updateAvailable') { controllers.update.updateAvailable(args[0] /* version */) } if (action === 'skipVersion') { controllers.update.skipVersion(args[0] /* version */) } - if (action === 'saveState') { - State.save(state) + + // Navigation between screens (back, forward, ESC, etc) + if (action === 'exitModal') { + state.modal = null + } + if (action === 'backToList') { + backToList() + } + if (action === 'escapeBack') { + escapeBack() + } + if (action === 'back') { + state.location.back() + } + if (action === 'forward') { + state.location.forward() + } + + // Controlling the window + if (action === 'setDimensions') { + setDimensions(args[0] /* dimensions */) + } + if (action === 'toggleFullScreen') { + ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */) } if (action === 'setTitle') { state.window.title = args[0] /* title */ } + + // Everything else + if (action === 'onOpen') { + onOpen(args[0] /* files */) + } + if (action === 'saveState') { + State.save(state) + } + if (action === 'onError') { + onError(args[0] /* user-friendly error */) + } if (action === 'uncaughtError') { telemetry.logUncaughtError(args[0] /* process */, args[1] /* error */) } @@ -318,6 +338,33 @@ function dispatch (action, ...args) { } } +// Listen to events from the main and webtorrent processes +function setupIpc () { + ipcRenderer.on('log', (e, ...args) => console.log(...args)) + ipcRenderer.on('error', (e, ...args) => console.error(...args)) + + ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args)) + + ipcRenderer.on('fullscreenChanged', onFullscreenChanged) + + ipcRenderer.on('wt-infohash', (e, ...args) => torrentInfoHash(...args)) + ipcRenderer.on('wt-metadata', (e, ...args) => torrentMetadata(...args)) + ipcRenderer.on('wt-done', (e, ...args) => torrentDone(...args)) + ipcRenderer.on('wt-warning', (e, ...args) => torrentWarning(...args)) + ipcRenderer.on('wt-error', (e, ...args) => torrentError(...args)) + + ipcRenderer.on('wt-progress', (e, ...args) => torrentProgress(...args)) + ipcRenderer.on('wt-file-modtimes', (e, ...args) => torrentFileModtimes(...args)) + ipcRenderer.on('wt-file-saved', (e, ...args) => torrentFileSaved(...args)) + ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args)) + ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args)) + ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args)) + + ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err)) + + ipcRenderer.send('ipcReady') +} + function play () { if (!state.playing.isPaused) return state.playing.isPaused = false @@ -441,39 +488,6 @@ function isCasting () { state.playing.location === 'dlna' } -function setupIpc () { - ipcRenderer.on('log', (e, ...args) => console.log(...args)) - ipcRenderer.on('error', (e, ...args) => console.error(...args)) - - ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args)) - - ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) { - state.window.isFullScreen = isFullScreen - if (!isFullScreen) { - // Aspect ratio gets reset in fullscreen mode, so restore it (OS X) - ipcRenderer.send('setAspectRatio', state.playing.aspectRatio) - } - update() - }) - - ipcRenderer.on('wt-infohash', (e, ...args) => torrentInfoHash(...args)) - ipcRenderer.on('wt-metadata', (e, ...args) => torrentMetadata(...args)) - ipcRenderer.on('wt-done', (e, ...args) => torrentDone(...args)) - ipcRenderer.on('wt-warning', (e, ...args) => torrentWarning(...args)) - ipcRenderer.on('wt-error', (e, ...args) => torrentError(...args)) - - ipcRenderer.on('wt-progress', (e, ...args) => torrentProgress(...args)) - ipcRenderer.on('wt-file-modtimes', (e, ...args) => torrentFileModtimes(...args)) - ipcRenderer.on('wt-file-saved', (e, ...args) => torrentFileSaved(...args)) - ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args)) - ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args)) - ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args)) - - ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err)) - - ipcRenderer.send('ipcReady') -} - // Starts all torrents that aren't paused on program startup function resumeTorrents () { state.saved.torrents @@ -481,35 +495,6 @@ function resumeTorrents () { .forEach((torrentSummary) => startTorrentingSummary(torrentSummary)) } -// Called when the user adds files (.torrent, files to seed, subtitles) to the app -// via any method (drag-drop, drag to app icon, command line) -function onOpen (files) { - if (!Array.isArray(files)) files = [ files ] - - if (state.modal) { - state.modal = null - } - - var subtitles = files.filter(isSubtitle) - - if (state.location.url() === 'home' || subtitles.length === 0) { - if (files.every(isTorrent)) { - if (state.location.url() !== 'home') { - backToList() - } - // All .torrent files? Add them. - files.forEach(addTorrent) - } else { - // Show the Create Torrent screen. Let's seed those files. - showCreateTorrent(files) - } - } else if (state.location.url() === 'player') { - addSubtitles(subtitles, true) - } - - update() -} - function isTorrent (file) { var name = typeof file === 'string' ? file : file.name var isTorrentFile = path.extname(name).toLowerCase() === '.torrent' @@ -526,28 +511,7 @@ function isSubtitle (file) { // Gets a torrent summary {name, infoHash, status} from state.saved.torrents // Returns undefined if we don't know that infoHash function getTorrentSummary (torrentKey) { - if (!torrentKey) return undefined - return state.saved.torrents.find((x) => - x.torrentKey === torrentKey || x.infoHash === torrentKey) -} - -// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a -// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent- -var instantIoRegex = /^(https:\/\/)?instant\.io\/#/ -function addTorrent (torrentId) { - backToList() - var torrentKey = state.nextTorrentKey++ - var path = state.saved.prefs.downloadPath - if (torrentId.path) { - // 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) - } - ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path) + return TorrentSummary.getByKey(state, torrentKey) } function addSubtitles (files, autoSelect) { @@ -661,90 +625,9 @@ function startTorrentingSummary (torrentSummary) { if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++ // Use Downloads folder by default - var path = s.path || state.saved.prefs.downloadPath + if (!s.path) s.path = state.saved.prefs.downloadPath - var torrentID - if (s.torrentFileName) { // Load torrent file from disk - torrentID = TorrentSummary.getTorrentPath(torrentSummary) - } else { // Load torrent from DHT - torrentID = s.magnetURI || s.infoHash - } - - console.log('start torrenting %s %s', s.torrentKey, torrentID) - ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes, s.selections) -} - -// -// TORRENT MANAGEMENT -// Send commands to the WebTorrent process, handle events -// - -// Shows the Create Torrent page with options to seed a given file or folder -function showCreateTorrent (files) { - // 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') { - state.location.go({ - url: 'create-torrent', - files: files - }) - return - } - - // ... or it will be an array of mixed file and folder paths. We have to walk - // through all the folders and find the files - findFilesRecursive(files, showCreateTorrent) -} - -// Recursively finds {name, path, size} for all files in a folder -// Calls `cb` on success, calls `onError` on failure -function findFilesRecursive (paths, cb) { - if (paths.length > 1) { - var numComplete = 0 - var ret = [] - paths.forEach(function (path) { - findFilesRecursive([path], function (fileObjs) { - ret = ret.concat(fileObjs) - if (++numComplete === paths.length) { - ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path) - cb(ret) - } - }) - }) - return - } - - var fileOrFolder = paths[0] - fs.stat(fileOrFolder, function (err, stat) { - if (err) return onError(err) - - // Files: return name, path, and size - if (!stat.isDirectory()) { - var filePath = fileOrFolder - return cb([{ - name: path.basename(filePath), - path: filePath, - size: stat.size - }]) - } - - // Folders: recurse, make a list of all the files - var folderPath = fileOrFolder - fs.readdir(folderPath, function (err, fileNames) { - if (err) return onError(err) - var paths = fileNames.map((fileName) => path.join(folderPath, fileName)) - findFilesRecursive(paths, cb) - }) - }) -} - -// Creates a new torrent and start seeeding -function createTorrent (options) { - var torrentKey = state.nextTorrentKey++ - ipcRenderer.send('wt-create-torrent', torrentKey, options) - state.location.backToFirst(function () { - state.location.clearForward('create-torrent') - }) + ipcRenderer.send('wt-start-torrenting', s) } function torrentInfoHash (torrentKey, infoHash) { @@ -1063,96 +946,6 @@ function openItem (infoHash, index) { ipcRenderer.send('openItem', filePath) } -// TODO: use torrentKey, not infoHash -function toggleTorrent (infoHash) { - var torrentSummary = getTorrentSummary(infoHash) - if (torrentSummary.status === 'paused') { - torrentSummary.status = 'new' - startTorrentingSummary(torrentSummary) - sound.play('ENABLE') - } else { - torrentSummary.status = 'paused' - ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash) - sound.play('DISABLE') - } -} - -// TODO: use torrentKey, not infoHash -function deleteTorrent (infoHash, deleteData) { - ipcRenderer.send('wt-stop-torrenting', infoHash) - - if (deleteData) { - var torrentSummary = getTorrentSummary(infoHash) - moveItemToTrash(torrentSummary) - } - - var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash) - if (index > -1) state.saved.torrents.splice(index, 1) - State.saveThrottled(state) - state.location.clearForward('player') // prevent user from going forward to a deleted torrent - sound.play('DELETE') -} - -function toggleSelectTorrent (infoHash) { - // toggle selection - state.selectedInfoHash = state.selectedInfoHash === infoHash ? null : infoHash - update() -} - -function toggleTorrentFile (infoHash, index) { - var torrentSummary = getTorrentSummary(infoHash) - torrentSummary.selections[index] = !torrentSummary.selections[index] - - // Let the WebTorrent process know to start or stop fetching that file - ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections) -} - -function openTorrentContextMenu (infoHash) { - var torrentSummary = getTorrentSummary(infoHash) - var menu = new electron.remote.Menu() - - menu.append(new electron.remote.MenuItem({ - label: 'Remove From List', - click: () => deleteTorrent(torrentSummary.infoHash, false) - })) - - menu.append(new electron.remote.MenuItem({ - label: 'Remove Data File', - click: () => deleteTorrent(torrentSummary.infoHash, true) - })) - - menu.append(new electron.remote.MenuItem({ - type: 'separator' - })) - - if (torrentSummary.files) { - menu.append(new electron.remote.MenuItem({ - label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder', - click: () => showItemInFolder(torrentSummary) - })) - menu.append(new electron.remote.MenuItem({ - type: 'separator' - })) - } - - menu.append(new electron.remote.MenuItem({ - label: 'Copy Magnet Link to Clipboard', - click: () => electron.clipboard.writeText(torrentSummary.magnetURI) - })) - - menu.append(new electron.remote.MenuItem({ - label: 'Copy Instant.io Link to Clipboard', - click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`) - })) - - menu.append(new electron.remote.MenuItem({ - label: 'Save Torrent File As...', - click: () => saveTorrentFileAs(torrentSummary) - })) - - menu.popup(electron.remote.getCurrentWindow()) -} - function getTorrentPath (torrentSummary) { var itemPath = path.join(torrentSummary.path, torrentSummary.files[0].path) if (torrentSummary.files.length > 1) { @@ -1161,35 +954,6 @@ function getTorrentPath (torrentSummary) { return itemPath } -function showItemInFolder (torrentSummary) { - ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary)) -} - -function moveItemToTrash (torrentSummary) { - ipcRenderer.send('moveItemToTrash', getTorrentPath(torrentSummary)) -} - -function saveTorrentFileAs (torrentSummary) { - var newFileName = `${path.parse(torrentSummary.name).name}.torrent` - var opts = { - title: 'Save Torrent File', - defaultPath: path.join(state.saved.prefs.downloadPath, newFileName), - filters: [ - { name: 'Torrent Files', extensions: ['torrent'] }, - { name: 'All Files', extensions: ['*'] } - ] - } - electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) { - var torrentPath = TorrentSummary.getTorrentPath(torrentSummary) - fs.readFile(torrentPath, function (err, torrentFile) { - if (err) return onError(err) - fs.writeFile(savePath, torrentFile, function (err) { - if (err) return onError(err) - }) - }) - }) -} - // Set window dimensions to match video dimensions or fill the screen function setDimensions (dimensions) { // Don't modify the window size if it's already maximized @@ -1268,7 +1032,35 @@ function showOrHidePlayerControls () { return false } -// Event handlers +// Called when the user adds files (.torrent, files to seed, subtitles) to the app +// via any method (drag-drop, drag to app icon, command line) +function onOpen (files) { + if (!Array.isArray(files)) files = [ files ] + + if (state.modal) { + state.modal = null + } + + var subtitles = files.filter(isSubtitle) + + if (state.location.url() === 'home' || subtitles.length === 0) { + if (files.every(isTorrent)) { + if (state.location.url() !== 'home') { + 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') { + addSubtitles(subtitles, true) + } + + update() +} + function onError (err) { console.error(err.stack || err) sound.play('ERROR') @@ -1276,6 +1068,7 @@ function onError (err) { time: new Date().getTime(), message: err.message || err }) + update() } @@ -1290,7 +1083,7 @@ function onPaste (e) { torrentIds.forEach(function (torrentId) { torrentId = torrentId.trim() if (torrentId.length === 0) return - addTorrent(torrentId) + controllers.torrentList.addTorrent(torrentId) }) update() @@ -1310,3 +1103,13 @@ function onBlur () { function onVisibilityChange () { state.window.isVisible = !document.webkitHidden } + +function onFullscreenChanged (e, isFullScreen) { + state.window.isFullScreen = isFullScreen + if (!isFullScreen) { + // Aspect ratio gets reset in fullscreen mode, so restore it (OS X) + ipcRenderer.send('setAspectRatio', state.playing.aspectRatio) + } + + update() +} diff --git a/renderer/webtorrent.js b/renderer/webtorrent.js index bce25a42..62d9471d 100644 --- a/renderer/webtorrent.js +++ b/renderer/webtorrent.js @@ -14,6 +14,7 @@ var path = require('path') var crashReporter = require('../crash-reporter') var config = require('../config') var torrentPoster = require('./lib/torrent-poster') +var TorrentSummary = require('./lib/torrent-summary') // Report when the process crashes crashReporter.init() @@ -42,8 +43,8 @@ function init () { client.on('warning', (err) => ipc.send('wt-warning', null, err.message)) client.on('error', (err) => ipc.send('wt-error', null, err.message)) - ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) => - startTorrenting(torrentKey, torrentID, path, fileModtimes, selections)) + ipc.on('wt-start-torrenting', (e, torrentSummary) => + startTorrenting(torrentSummary)) ipc.on('wt-stop-torrenting', (e, infoHash) => stopTorrenting(infoHash)) ipc.on('wt-create-torrent', (e, torrentKey, options) => @@ -72,12 +73,15 @@ function init () { // Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object // See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent- -function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) { +function startTorrenting (torrentSummary) { + var s = torrentSummary + var torrentKey = s.torrentKey + var torrentID = TorrentSummary.getTorrentID(s) console.log('starting torrent %s: %s', torrentKey, torrentID) var torrent = client.add(torrentID, { - path: path, - fileModtimes: fileModtimes + path: s.path, + fileModtimes: s.fileModtimes }) torrent.key = torrentKey @@ -85,7 +89,7 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) addTorrentEvents(torrent) // Only download the files the user wants, not necessarily all files - torrent.once('ready', () => selectFiles(torrent, selections)) + torrent.once('ready', () => selectFiles(torrent, s.selections)) return torrent }