From 6e240b3fd492481eb6c7fc464468ceb15edca380 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Thu, 26 May 2016 17:47:16 -0700 Subject: [PATCH 01/11] Misc file moving and cleanup - Rename JS/CSS for main.html to be consistent (main.js, main.css) - Add hx.js module to reduce virtual-dom boilerplate - Move state.js into renderer/lib.js where it belongs - Rename torrent-list.js -> home.js for consistency - Rename create-torrent-page.js -> create-torrent.js for consistency --- renderer/lib/dispatcher.js | 17 +++++++++++------ renderer/lib/hx.js | 5 +++++ renderer/{ => lib}/state.js | 4 ++-- renderer/{index.css => main.css} | 18 +++++++++--------- renderer/main.html | 4 ++-- renderer/{index.js => main.js} | 2 +- renderer/views/app.js | 11 +++++------ ...reate-torrent-page.js => create-torrent.js} | 9 +++------ renderer/views/header.js | 5 +---- renderer/views/{torrent-list.js => home.js} | 12 ++++-------- renderer/views/open-torrent-address-modal.js | 5 +---- renderer/views/player.js | 5 +---- renderer/views/preferences.js | 4 +--- renderer/views/unsupported-media-modal.js | 5 +---- renderer/views/update-available-modal.js | 5 +---- 15 files changed, 48 insertions(+), 63 deletions(-) create mode 100644 renderer/lib/hx.js rename renderer/{ => lib}/state.js (99%) rename renderer/{index.css => main.css} (98%) rename renderer/{index.js => main.js} (99%) rename renderer/views/{create-torrent-page.js => create-torrent.js} (97%) rename renderer/views/{torrent-list.js => home.js} (96%) diff --git a/renderer/lib/dispatcher.js b/renderer/lib/dispatcher.js index 13778d99..98d590a3 100644 --- a/renderer/lib/dispatcher.js +++ b/renderer/lib/dispatcher.js @@ -8,6 +8,7 @@ module.exports = { // () => dispatch() // ... this prevents virtual-dom from updating every listener on every update() var _dispatchers = {} + var _dispatch = () => {} function setDispatch (dispatch) { @@ -17,14 +18,18 @@ function setDispatch (dispatch) { // Get a _memoized event handler that calls dispatch() // All args must be JSON-able function dispatcher (...args) { - var json = JSON.stringify(args) - var handler = _dispatchers[json] + var str = JSON.stringify(args) + var handler = _dispatchers[str] if (!handler) { - handler = _dispatchers[json] = (e) => { - // Don't click on whatever is below the button + handler = _dispatchers[str] = function (e) { + // Do not propagate click to elements below the button e.stopPropagation() - // Don't regisiter clicks on disabled buttons - if (e.currentTarget.classList.contains('disabled')) return + + if (e.currentTarget.classList.contains('disabled')) { + // Do not allow clicks on disabled buttons + return + } + _dispatch.apply(null, args) } } diff --git a/renderer/lib/hx.js b/renderer/lib/hx.js new file mode 100644 index 00000000..d1434273 --- /dev/null +++ b/renderer/lib/hx.js @@ -0,0 +1,5 @@ +var h = require('virtual-dom/h') +var hyperx = require('hyperx') +var hx = hyperx(h) + +module.exports = hx diff --git a/renderer/state.js b/renderer/lib/state.js similarity index 99% rename from renderer/state.js rename to renderer/lib/state.js index 40796f62..16c93a55 100644 --- a/renderer/state.js +++ b/renderer/lib/state.js @@ -3,8 +3,8 @@ var path = require('path') var remote = electron.remote -var config = require('../config') -var LocationHistory = require('./lib/location-history') +var config = require('../../config') +var LocationHistory = require('./location-history') module.exports = { getInitialState, diff --git a/renderer/index.css b/renderer/main.css similarity index 98% rename from renderer/index.css rename to renderer/main.css index 4d28292d..b8cc8ac4 100644 --- a/renderer/index.css +++ b/renderer/main.css @@ -280,36 +280,36 @@ table { width: 100%; } -.create-torrent-page { +.create-torrent { padding: 10px 25px; overflow: hidden; } -.create-torrent-page .torrent-attribute { +.create-torrent .torrent-attribute { white-space: nowrap; } -.create-torrent-page .torrent-attribute>* { +.create-torrent .torrent-attribute>* { display: inline-block; } -.create-torrent-page .torrent-attribute label { +.create-torrent .torrent-attribute label { width: 60px; margin-right: 10px; vertical-align: top; } -.create-torrent-page .torrent-attribute>div { +.create-torrent .torrent-attribute>div { width: calc(100% - 90px); } -.create-torrent-page .torrent-attribute div { +.create-torrent .torrent-attribute div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.create-torrent-page .torrent-attribute textarea { +.create-torrent .torrent-attribute textarea { width: calc(100% - 80px); height: 80px; color: #eee; @@ -321,11 +321,11 @@ table { padding: 4px 6px; } -.create-torrent-page textarea.torrent-trackers { +.create-torrent textarea.torrent-trackers { height: 200px; } -.create-torrent-page input.torrent-is-private { +.create-torrent input.torrent-is-private { width: initial; margin: 0; } diff --git a/renderer/main.html b/renderer/main.html index 4d04d3cb..e2f14ed4 100644 --- a/renderer/main.html +++ b/renderer/main.html @@ -3,9 +3,9 @@ - + - + diff --git a/renderer/index.js b/renderer/main.js similarity index 99% rename from renderer/index.js rename to renderer/main.js index 0d4acf50..fe97964f 100644 --- a/renderer/index.js +++ b/renderer/main.js @@ -28,7 +28,7 @@ var App = require('./views/app') var config = require('../config') var errors = require('./lib/errors') var sound = require('./lib/sound') -var State = require('./state') +var State = require('./lib/state') var TorrentPlayer = require('./lib/torrent-player') var TorrentSummary = require('./lib/torrent-summary') diff --git a/renderer/views/app.js b/renderer/views/app.js index d5dd1cc1..6dbad519 100644 --- a/renderer/views/app.js +++ b/renderer/views/app.js @@ -1,16 +1,15 @@ module.exports = App -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) - +var hx = require('../lib/hx') var Header = require('./header') + var Views = { - 'home': require('./torrent-list'), + 'home': require('./home'), 'player': require('./player'), - 'create-torrent': require('./create-torrent-page'), + 'create-torrent': require('./create-torrent'), 'preferences': require('./preferences') } + var Modals = { 'open-torrent-address-modal': require('./open-torrent-address-modal'), 'update-available-modal': require('./update-available-modal'), diff --git a/renderer/views/create-torrent-page.js b/renderer/views/create-torrent.js similarity index 97% rename from renderer/views/create-torrent-page.js rename to renderer/views/create-torrent.js index 495b33a2..28c8f5fd 100644 --- a/renderer/views/create-torrent-page.js +++ b/renderer/views/create-torrent.js @@ -1,14 +1,11 @@ module.exports = CreateTorrentPage -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) - var createTorrent = require('create-torrent') var path = require('path') var prettyBytes = require('prettier-bytes') var {dispatch, dispatcher} = require('../lib/dispatcher') +var hx = require('../lib/hx') function CreateTorrentPage (state) { var info = state.location.current() @@ -59,7 +56,7 @@ function CreateTorrentPage (state) { var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed' return hx` -
+

Create torrent ${defaultName}

${torrentInfo} @@ -132,7 +129,7 @@ function CreateTorrentPage (state) { function CreateTorrentErrorPage () { return hx` -

+

Create torrent

diff --git a/renderer/views/header.js b/renderer/views/header.js index ab777ca7..67e74114 100644 --- a/renderer/views/header.js +++ b/renderer/views/header.js @@ -1,10 +1,7 @@ module.exports = Header -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) - var {dispatcher} = require('../lib/dispatcher') +var hx = require('../lib/hx') function Header (state) { return hx` diff --git a/renderer/views/torrent-list.js b/renderer/views/home.js similarity index 96% rename from renderer/views/torrent-list.js rename to renderer/views/home.js index 7bcd2dc2..77a5070c 100644 --- a/renderer/views/torrent-list.js +++ b/renderer/views/home.js @@ -1,17 +1,17 @@ module.exports = TorrentList -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) var prettyBytes = require('prettier-bytes') +var hx = require('../lib/hx') var TorrentSummary = require('../lib/torrent-summary') var TorrentPlayer = require('../lib/torrent-player') var {dispatcher} = require('../lib/dispatcher') function TorrentList (state) { var torrentRows = state.saved.torrents.map( - (torrentSummary) => renderTorrent(torrentSummary)) + (torrentSummary) => renderTorrent(torrentSummary) + ) + return hx`

${torrentRows} @@ -20,11 +20,7 @@ function TorrentList (state) {
` - // Renders a torrent in the torrent list - // Includes name, download status, play button, background image - // May be expanded for additional info, including the list of files inside function renderTorrent (torrentSummary) { - // Get ephemeral data (like progress %) directly from the WebTorrent handle var infoHash = torrentSummary.infoHash var isSelected = infoHash && state.selectedInfoHash === infoHash diff --git a/renderer/views/open-torrent-address-modal.js b/renderer/views/open-torrent-address-modal.js index 2019efbe..1cf9bb40 100644 --- a/renderer/views/open-torrent-address-modal.js +++ b/renderer/views/open-torrent-address-modal.js @@ -1,10 +1,7 @@ module.exports = OpenTorrentAddressModal -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) - var {dispatch} = require('../lib/dispatcher') +var hx = require('../lib/hx') function OpenTorrentAddressModal (state) { return hx` diff --git a/renderer/views/player.js b/renderer/views/player.js index 039848df..ff4f1f8e 100644 --- a/renderer/views/player.js +++ b/renderer/views/player.js @@ -1,13 +1,10 @@ module.exports = Player -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) - var Bitfield = require('bitfield') var prettyBytes = require('prettier-bytes') var zeroFill = require('zero-fill') +var hx = require('../lib/hx') var TorrentSummary = require('../lib/torrent-summary') var {dispatch, dispatcher} = require('../lib/dispatcher') diff --git a/renderer/views/preferences.js b/renderer/views/preferences.js index f9af1a62..4e4773ed 100644 --- a/renderer/views/preferences.js +++ b/renderer/views/preferences.js @@ -1,8 +1,6 @@ module.exports = Preferences -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) +var hx = require('../lib/hx') var {dispatch} = require('../lib/dispatcher') var remote = require('electron').remote diff --git a/renderer/views/unsupported-media-modal.js b/renderer/views/unsupported-media-modal.js index 9cd04e0e..a6bb9d5c 100644 --- a/renderer/views/unsupported-media-modal.js +++ b/renderer/views/unsupported-media-modal.js @@ -1,12 +1,9 @@ module.exports = UnsupportedMediaModal -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) - var electron = require('electron') var {dispatch, dispatcher} = require('../lib/dispatcher') +var hx = require('../lib/hx') function UnsupportedMediaModal (state) { var err = state.modal.error diff --git a/renderer/views/update-available-modal.js b/renderer/views/update-available-modal.js index 150de2cd..d2709740 100644 --- a/renderer/views/update-available-modal.js +++ b/renderer/views/update-available-modal.js @@ -1,12 +1,9 @@ module.exports = UpdateAvailableModal -var h = require('virtual-dom/h') -var hyperx = require('hyperx') -var hx = hyperx(h) - var electron = require('electron') var {dispatch} = require('../lib/dispatcher') +var hx = require('../lib/hx') function UpdateAvailableModal (state) { return hx` From 1aabd537d8809cd0eb5dac69f78cad826a209059 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Thu, 26 May 2016 18:12:23 -0700 Subject: [PATCH 02/11] cleanup dispatcher --- renderer/lib/dispatcher.js | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/renderer/lib/dispatcher.js b/renderer/lib/dispatcher.js index 98d590a3..eba84df7 100644 --- a/renderer/lib/dispatcher.js +++ b/renderer/lib/dispatcher.js @@ -1,41 +1,39 @@ module.exports = { - setDispatch, dispatch, - dispatcher + dispatcher, + setDispatch } -// Memoize most of our event handlers, which are functions in the form -// () => dispatch() -// ... this prevents virtual-dom from updating every listener on every update() -var _dispatchers = {} - -var _dispatch = () => {} +var dispatchers = {} +var _dispatch = function () {} function setDispatch (dispatch) { _dispatch = dispatch } -// Get a _memoized event handler that calls dispatch() -// All args must be JSON-able +function dispatch (...args) { + _dispatch(...args) +} + +// Most DOM event handlers are trivial functions like `() => dispatch()`. +// For these, `dispatcher()` is preferred because it memoizes the handler +// function. This prevents virtual-dom from updating the listener functions on +// each update(). function dispatcher (...args) { var str = JSON.stringify(args) - var handler = _dispatchers[str] + var handler = dispatchers[str] if (!handler) { - handler = _dispatchers[str] = function (e) { + handler = dispatchers[str] = function (e) { // Do not propagate click to elements below the button e.stopPropagation() if (e.currentTarget.classList.contains('disabled')) { - // Do not allow clicks on disabled buttons + // Ignore clicks on disabled elements return } - _dispatch.apply(null, args) + dispatch(...args) } } return handler } - -function dispatch (...args) { - _dispatch.apply(null, args) -} From 9abab7aec3b5ad9d3c5ca91ff4eb22cc2e376632 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Thu, 26 May 2016 18:29:07 -0700 Subject: [PATCH 03/11] cleanup check-deps --- bin/check-deps.js | 47 +++++++++++++++++++++++++---------------------- bin/cmd.js | 2 +- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/bin/check-deps.js b/bin/check-deps.js index 4b3f48ef..6f212295 100755 --- a/bin/check-deps.js +++ b/bin/check-deps.js @@ -3,49 +3,52 @@ var fs = require('fs') var cp = require('child_process') -var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen'] +var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path'] var EXECUTABLE_DEPS = ['gh-release', 'standard'] main() -// Scans our codebase and package.json for missing or unused dependencies -// Process returns 0 on success, prints a message and returns 1 on failure +// Scans codebase for missing or unused dependencies. Exits with code 0 on success. function main () { if (process.platform === 'win32') { - console.log('Sorry, check-deps only works on Mac and Linux') + console.error('Sorry, check-deps only works on Mac and Linux') return } - var jsDeps = findJSDeps() + var usedDeps = findUsedDeps() var packageDeps = findPackageDeps() - var missingDeps = jsDeps.filter((dep) => - packageDeps.indexOf(dep) < 0 && - BUILT_IN_DEPS.indexOf(dep) < 0) - var unusedDeps = packageDeps.filter((dep) => - jsDeps.indexOf(dep) < 0 && - EXECUTABLE_DEPS.indexOf(dep) < 0) + var missingDeps = usedDeps.filter( + (dep) => !packageDeps.includes(dep) && !BUILT_IN_DEPS.includes(dep) + ) + var unusedDeps = packageDeps.filter( + (dep) => !usedDeps.includes(dep) && !EXECUTABLE_DEPS.includes(dep) + ) - if (missingDeps.length > 0) console.log('Missing package dependencies: ' + missingDeps) - if (unusedDeps.length > 0) console.log('Unused package dependencies: ' + unusedDeps) - - if (missingDeps.length + unusedDeps.length > 0) process.exit(1) - - console.log('Lookin good!') + if (missingDeps.length > 0) { + console.error('Missing package dependencies: ' + missingDeps) + } + if (unusedDeps.length > 0) { + console.error('Unused package dependencies: ' + unusedDeps) + } + if (missingDeps.length + unusedDeps.length > 0) { + process.exitCode = 1 + } } -// Finds all dependencies, required, optional, or dev, in package.json +// Finds all dependencies specified in `package.json` function findPackageDeps () { var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')) - var requiredDeps = Object.keys(pkg.dependencies) + + var deps = Object.keys(pkg.dependencies) var devDeps = Object.keys(pkg.devDependencies) var optionalDeps = Object.keys(pkg.optionalDependencies) - return [].concat(requiredDeps, devDeps, optionalDeps) + return [].concat(deps, devDeps, optionalDeps) } -// Finds all dependencies required() in the code -function findJSDeps () { +// Finds all dependencies that used with `require()` +function findUsedDeps () { var stdout = cp.execSync('./bin/list-deps.sh') return stdout.toString().trim().split('\n') } diff --git a/bin/cmd.js b/bin/cmd.js index 3723bbb8..e18f3268 100755 --- a/bin/cmd.js +++ b/bin/cmd.js @@ -6,5 +6,5 @@ var path = require('path') var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'}) child.on('close', function (code) { - process.exit(code) + process.exitCode = code }) From 3757507b180197a6614fc6e8da09ee7edebd8823 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Thu, 26 May 2016 18:41:48 -0700 Subject: [PATCH 04/11] cleanup announcement --- main/announcement.js | 48 ++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/main/announcement.js b/main/announcement.js index 1e2801d5..1959b886 100644 --- a/main/announcement.js +++ b/main/announcement.js @@ -13,26 +13,30 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL + '&platform=' + process.platform function init () { - get.concat(ANNOUNCEMENT_URL, function (err, res, data) { - if (err) return log('failed to retrieve remote message') - if (res.statusCode !== 200) return log('no remote message') - - try { - data = JSON.parse(data.toString()) - } catch (err) { - data = { - title: 'WebTorrent Desktop Announcement', - message: 'WebTorrent Desktop Announcement', - detail: data.toString() - } - } - - electron.dialog.showMessageBox({ - type: 'info', - buttons: ['OK'], - title: data.title, - message: data.message, - detail: data.detail - }, function () {}) - }) + get.concat(ANNOUNCEMENT_URL, onResponse) } + +function onResponse (err, res, data) { + if (err) return log('failed to retrieve remote message') + if (res.statusCode !== 200) return log('no remote message') + + try { + data = JSON.parse(data.toString()) + } catch (err) { + data = { + title: 'WebTorrent Desktop Announcement', + message: 'Announcement', + detail: data.toString() + } + } + + electron.dialog.showMessageBox({ + type: 'info', + buttons: ['OK'], + title: data.title, + message: data.message, + detail: data.detail + }, noop) +} + +function noop () {} From 001601bc5ff5078166166cc07ca35dcaf815487d Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Fri, 27 May 2016 00:01:30 -0700 Subject: [PATCH 05/11] Major refactor -- split windows into separate files --- main/announcement.js | 2 +- main/dialog.js | 53 +++++++++++++ main/index.js | 43 +++++------ main/ipc.js | 60 ++++++--------- main/log.js | 5 ++ main/menu.js | 154 ++++++++++--------------------------- main/power-save-blocker.js | 25 ++++++ main/tray.js | 98 ++++++++++++++--------- main/windows.js | 145 ---------------------------------- main/windows/about.js | 50 ++++++++++++ main/windows/index.js | 3 + main/windows/main.js | 115 +++++++++++++++++++++++++++ main/windows/util.js | 11 +++ main/windows/webtorrent.js | 48 ++++++++++++ renderer/main.js | 10 ++- 15 files changed, 464 insertions(+), 358 deletions(-) create mode 100644 main/dialog.js create mode 100644 main/power-save-blocker.js delete mode 100644 main/windows.js create mode 100644 main/windows/about.js create mode 100644 main/windows/index.js create mode 100644 main/windows/main.js create mode 100644 main/windows/util.js create mode 100644 main/windows/webtorrent.js diff --git a/main/announcement.js b/main/announcement.js index 1959b886..b6977533 100644 --- a/main/announcement.js +++ b/main/announcement.js @@ -3,7 +3,6 @@ module.exports = { } var electron = require('electron') -var get = require('simple-get') var config = require('../config') var log = require('./log') @@ -13,6 +12,7 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL + '&platform=' + process.platform function init () { + var get = require('simple-get') get.concat(ANNOUNCEMENT_URL, onResponse) } diff --git a/main/dialog.js b/main/dialog.js new file mode 100644 index 00000000..1aaab35f --- /dev/null +++ b/main/dialog.js @@ -0,0 +1,53 @@ +module.exports = { + openSeedFile, + openSeedDirectory, + openTorrentFile, + openTorrentAddress +} + +var electron = require('electron') +var windows = require('./windows') + +// Prompts the user for a file, then creates a torrent. Only allows a single file +// selection. +function openSeedFile () { + electron.dialog.showOpenDialog({ + title: 'Select a file for the torrent file.', + properties: [ 'openFile' ] + }, function (selectedPaths) { + if (!Array.isArray(selectedPaths)) return + windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) + }) +} + +// Prompts the user for a file or directory, then creates a torrent. Only allows a +// single selection. To create a multi-file torrent, the user must select a +// directory. +function openSeedDirectory () { + electron.dialog.showOpenDialog({ + title: 'Select a file or folder for the torrent file.', + properties: [ 'openFile', 'openDirectory' ] + }, function (selectedPaths) { + if (!Array.isArray(selectedPaths)) return + windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) + }) +} + +// Prompts the user to choose a torrent file, then adds it. +function openTorrentFile () { + electron.dialog.showOpenDialog(windows.main.win, { + title: 'Select a .torrent file to open.', + filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], + properties: [ 'openFile', 'multiSelections' ] + }, function (selectedPaths) { + if (!Array.isArray(selectedPaths)) return + selectedPaths.forEach(function (selectedPath) { + windows.main.send('dispatch', 'addTorrent', selectedPath) + }) + }) +} + +// Prompts the user for the URL of a torrent file, then downloads and adds it +function openTorrentAddress () { + windows.main.send('showOpenTorrentAddress') +} diff --git a/main/index.js b/main/index.js index 92f7dacb..79ad6389 100644 --- a/main/index.js +++ b/main/index.js @@ -8,6 +8,7 @@ var ipcMain = electron.ipcMain var announcement = require('./announcement') var config = require('../config') var crashReporter = require('../crash-reporter') +var dialog = require('./dialog') var handlers = require('./handlers') var ipc = require('./ipc') var log = require('./log') @@ -60,8 +61,8 @@ function init () { app.on('ready', function () { isReady = true - windows.createMainWindow() - windows.createWebTorrentHiddenWindow() + windows.main.create() + windows.webtorrent.create() menu.init() // To keep app startup fast, some code is delayed. @@ -79,13 +80,13 @@ function init () { app.isQuitting = true e.preventDefault() - windows.main.send('dispatch', 'saveState') /* try to save state on exit */ + windows.main.send('dispatch', 'saveState') // try to save state on exit ipcMain.once('savedState', () => app.quit()) - setTimeout(() => app.quit(), 2000) /* quit after 2 secs, at most */ + setTimeout(() => app.quit(), 2000) // quit after 2 secs, at most }) app.on('activate', function () { - if (isReady) windows.createMainWindow() + if (isReady) windows.main.create() }) } @@ -101,11 +102,11 @@ function onOpen (e, torrentId) { if (app.ipcReady) { windows.main.send('dispatch', 'onOpen', torrentId) - // Magnet links opened from Chrome won't focus the app without a setTimeout. The - // confirmation dialog Chrome shows causes Chrome to steal back the focus. + // Magnet links opened from Chrome won't focus the app without a setTimeout. + // The confirmation dialog Chrome shows causes Chrome to steal back the focus. // Electron issue: https://github.com/atom/electron/issues/4338 setTimeout(function () { - windows.focusWindow(windows.main) + windows.main.focus() }, 100) } else { argv.push(torrentId) @@ -114,10 +115,11 @@ function onOpen (e, torrentId) { function onAppOpen (newArgv) { newArgv = sliceArgv(newArgv) + console.log(newArgv) if (app.ipcReady) { log('Second app instance opened, but was prevented:', newArgv) - windows.focusWindow(windows.main) + windows.main.focus() processArgv(newArgv) } else { @@ -130,27 +132,22 @@ function sliceArgv (argv) { } function processArgv (argv) { - var pathsToOpen = [] + var paths = [] argv.forEach(function (arg) { if (arg === '-n') { - menu.showOpenSeedFiles() + dialog.openSeedDirectory() } else if (arg === '-o') { - menu.showOpenTorrentFile() + dialog.openTorrentFile() } else if (arg === '-u') { - menu.showOpenTorrentAddress() + dialog.openTorrentAddress() } else if (arg.startsWith('-psn')) { // Ignore OS X launchd "process serial number" argument - // More: https://github.com/feross/webtorrent-desktop/issues/214 + // Issue: https://github.com/feross/webtorrent-desktop/issues/214 } else { - pathsToOpen.push(arg) + paths.push(arg) } }) - if (pathsToOpen.length > 0) openFilePaths(pathsToOpen) -} - -// Send files to the renderer process -// Opening files means either adding torrents, creating and seeding a torrent -// from files, or adding subtitles -function openFilePaths (paths) { - windows.main.send('dispatch', 'onOpen', paths) + if (paths.length > 0) { + windows.main.send('dispatch', 'onOpen', paths) + } } diff --git a/main/ipc.js b/main/ipc.js index 17bec585..6498f21e 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -5,24 +5,23 @@ module.exports = { var electron = require('electron') var app = electron.app -var ipcMain = electron.ipcMain var log = require('./log') var menu = require('./menu') -var windows = require('./windows') +var powerSaveBlocker = require('./power-save-blocker') var shortcuts = require('./shortcuts') var vlc = require('./vlc') +var windows = require('./windows') -// has to be a number, not a boolean, and undefined throws an error -var powerSaveBlockerId = 0 - -// messages from the main process, to be sent once the WebTorrent process starts +// Messages from the main process, to be sent once the WebTorrent process starts var messageQueueMainToWebTorrent = [] // holds a ChildProcess while we're playing a video in VLC, null otherwise var vlcProcess function init () { + var ipcMain = electron.ipcMain + ipcMain.on('ipcReady', function (e) { windows.main.show() app.ipcReady = true @@ -39,7 +38,7 @@ function init () { }) }) - ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile) + ipcMain.on('showOpenTorrentFile', () => menu.showOpenTorrentFile()) ipcMain.on('setBounds', function (e, bounds, maximize) { setBounds(bounds, maximize) @@ -58,11 +57,11 @@ function init () { }) ipcMain.on('toggleFullScreen', function (e, flag) { - menu.toggleFullScreen(flag) + windows.main.toggleFullScreen(flag) }) ipcMain.on('setTitle', function (e, title) { - windows.main.setTitle(title) + windows.main.win.setTitle(title) }) ipcMain.on('openItem', function (e, path) { @@ -75,9 +74,8 @@ function init () { electron.shell.showItemInFolder(path) }) - ipcMain.on('blockPowerSave', blockPowerSave) - - ipcMain.on('unblockPowerSave', unblockPowerSave) + ipcMain.on('blockPowerSave', () => powerSaveBlocker.start()) + ipcMain.on('unblockPowerSave', () => powerSaveBlocker.stop()) ipcMain.on('onPlayerOpen', function () { menu.onPlayerOpen() @@ -174,7 +172,7 @@ function init () { function setBounds (bounds, maximize) { // Do nothing in fullscreen - if (!windows.main || windows.main.isFullScreen()) { + if (!windows.main.win || windows.main.win.isFullScreen()) { log('setBounds: not setting bounds because we\'re in full screen') return } @@ -182,19 +180,19 @@ function setBounds (bounds, maximize) { // Maximize or minimize, if the second argument is present var willBeMaximized if (maximize === true) { - if (!windows.main.isMaximized()) { + if (!windows.main.win.isMaximized()) { log('setBounds: maximizing') - windows.main.maximize() + windows.main.win.maximize() } willBeMaximized = true } else if (maximize === false) { - if (windows.main.isMaximized()) { + if (windows.main.win.isMaximized()) { log('setBounds: unmaximizing') - windows.main.unmaximize() + windows.main.win.unmaximize() } willBeMaximized = false } else { - willBeMaximized = windows.main.isMaximized() + willBeMaximized = windows.main.win.isMaximized() } // Assuming we're not maximized or maximizing, set the window size @@ -202,12 +200,12 @@ function setBounds (bounds, maximize) { log('setBounds: setting bounds to ' + JSON.stringify(bounds)) if (bounds.x === null && bounds.y === null) { // X and Y not specified? By default, center on current screen - var scr = electron.screen.getDisplayMatching(windows.main.getBounds()) + var scr = electron.screen.getDisplayMatching(windows.main.win.getBounds()) bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2) bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2) log('setBounds: centered to ' + JSON.stringify(bounds)) } - windows.main.setBounds(bounds, true) + windows.main.win.setBounds(bounds, true) } else { log('setBounds: not setting bounds because of window maximization') } @@ -215,9 +213,8 @@ function setBounds (bounds, maximize) { function setAspectRatio (aspectRatio) { log('setAspectRatio %o', aspectRatio) - if (windows.main) { - windows.main.setAspectRatio(aspectRatio) - } + if (!windows.main.win) return + windows.main.win.setAspectRatio(aspectRatio) } // Display string in dock badging area (OS X) @@ -231,19 +228,6 @@ function setBadge (text) { // Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1. function setProgress (progress) { log('setProgress %s', progress) - if (windows.main) { - windows.main.setProgressBar(progress) - } -} - -function blockPowerSave () { - powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep') - log('blockPowerSave %d', powerSaveBlockerId) -} - -function unblockPowerSave () { - if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) { - electron.powerSaveBlocker.stop(powerSaveBlockerId) - log('unblockPowerSave %d', powerSaveBlockerId) - } + if (!windows.main.win) return + windows.main.win.setProgressBar(progress) } diff --git a/main/log.js b/main/log.js index bc064135..08bc7b12 100644 --- a/main/log.js +++ b/main/log.js @@ -10,11 +10,16 @@ module.exports.error = error var electron = require('electron') +var config = require('../config') var windows = require('./windows') var app = electron.app function log (...args) { + if (!config.IS_PRODUCTION) { + // In development, also log to the console + console.log(...args) + } if (app.ipcReady) { windows.main.send('log', ...args) } else { diff --git a/main/menu.js b/main/menu.js index 6a3b8891..730383d1 100644 --- a/main/menu.js +++ b/main/menu.js @@ -4,13 +4,7 @@ module.exports = { onPlayerOpen, onToggleFullScreen, onWindowHide, - onWindowShow, - - // TODO: move these out of menu.js -- they don't belong here - showOpenSeedFiles, - showOpenTorrentAddress, - showOpenTorrentFile, - toggleFullScreen + onWindowShow } var electron = require('electron') @@ -18,6 +12,7 @@ var electron = require('electron') var app = electron.app var config = require('../config') +var dialog = require('./dialog') var log = require('./log') var windows = require('./windows') @@ -33,100 +28,76 @@ function init () { } } -function toggleFullScreen (flag) { - log('toggleFullScreen %s', flag) - if (windows.main && windows.main.isVisible()) { - flag = flag != null ? flag : !windows.main.isFullScreen() - if (flag) { - // Allows the window to use the full screen in fullscreen mode (OS X). - windows.main.setAspectRatio(0) - } - windows.main.setFullScreen(flag) - } -} - // Sets whether the window should always show on top of other windows function toggleFloatOnTop (flag) { + if (!windows.main.win) return log('toggleFloatOnTop %s', flag) - if (windows.main) { - flag = flag != null ? flag : !windows.main.isAlwaysOnTop() - windows.main.setAlwaysOnTop(flag) - getMenuItem('Float on Top').checked = flag - } + flag = flag != null ? flag : !windows.main.isAlwaysOnTop() + windows.main.setAlwaysOnTop(flag) + getMenuItem('Float on Top').checked = flag } function toggleDevTools () { + if (!windows.main.win) return log('toggleDevTools') - if (windows.main) { - windows.main.toggleDevTools() - } + windows.main.toggleDevTools() } function showWebTorrentWindow () { log('showWebTorrentWindow') windows.webtorrent.show() - windows.webtorrent.webContents.openDevTools({ detach: true }) + windows.webtorrent.win.webContents.openDevTools({ detach: true }) } function playPause () { - if (windows.main) { - windows.main.send('dispatch', 'playPause') - } + if (!windows.main.win) return + windows.main.send('dispatch', 'playPause') } function increaseVolume () { - if (windows.main) { - windows.main.send('dispatch', 'changeVolume', 0.1) - } + if (!windows.main.win) return + windows.main.send('dispatch', 'changeVolume', 0.1) } function decreaseVolume () { - if (windows.main) { - windows.main.send('dispatch', 'changeVolume', -0.1) - } + if (!windows.main.win) return + windows.main.send('dispatch', 'changeVolume', -0.1) } function openSubtitles () { - if (windows.main) { - windows.main.send('dispatch', 'openSubtitles') - } + if (!windows.main.win) return + windows.main.send('dispatch', 'openSubtitles') } function skipForward () { - if (windows.main) { - windows.main.send('dispatch', 'skip', 1) - } + if (!windows.main.win) return + windows.main.send('dispatch', 'skip', 1) } function skipBack () { - if (windows.main) { - windows.main.send('dispatch', 'skip', -1) - } + if (!windows.main.win) return + windows.main.send('dispatch', 'skip', -1) } function increasePlaybackRate () { - if (windows.main) { - windows.main.send('dispatch', 'changePlaybackRate', 1) - } + if (!windows.main.win) return + windows.main.send('dispatch', 'changePlaybackRate', 1) } function decreasePlaybackRate () { - if (windows.main) { - windows.main.send('dispatch', 'changePlaybackRate', -1) - } + if (!windows.main.win) return + windows.main.send('dispatch', 'changePlaybackRate', -1) } // Open the preferences window function showPreferences () { - if (windows.main) { - windows.main.send('dispatch', 'preferences') - } + if (!windows.main.win) return + windows.main.send('dispatch', 'preferences') } function escapeBack () { - if (windows.main) { - windows.main.send('dispatch', 'escapeBack') - } + if (!windows.main.win) return + windows.main.send('dispatch', 'escapeBack') } function onWindowShow () { @@ -166,8 +137,10 @@ function onPlayerClose () { } function onToggleFullScreen (isFullScreen) { - isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen() - windows.main.setMenuBarVisibility(!isFullScreen) + if (isFullScreen == null) { + isFullScreen = windows.main.win.isFullScreen() + } + windows.main.win.setMenuBarVisibility(!isFullScreen) getMenuItem('Full Screen').checked = isFullScreen windows.main.send('fullscreenChanged', isFullScreen) } @@ -181,49 +154,6 @@ function getMenuItem (label) { } } -// Prompts the user for a file, then creates a torrent. Only allows a single file -// selection. -function showOpenSeedFile () { - electron.dialog.showOpenDialog({ - title: 'Select a file for the torrent file.', - properties: [ 'openFile' ] - }, function (selectedPaths) { - if (!Array.isArray(selectedPaths)) return - windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) - }) -} - -// Prompts the user for a file or directory, then creates a torrent. Only allows a single -// selection. To create a multi-file torrent, the user must select a directory. -function showOpenSeedFiles () { - electron.dialog.showOpenDialog({ - title: 'Select a file or folder for the torrent file.', - properties: [ 'openFile', 'openDirectory' ] - }, function (selectedPaths) { - if (!Array.isArray(selectedPaths)) return - windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) - }) -} - -// Prompts the user to choose a torrent file, then adds it to the app -function showOpenTorrentFile () { - electron.dialog.showOpenDialog(windows.main, { - title: 'Select a .torrent file to open.', - filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], - properties: [ 'openFile', 'multiSelections' ] - }, function (selectedPaths) { - if (!Array.isArray(selectedPaths)) return - selectedPaths.forEach(function (selectedPath) { - windows.main.send('dispatch', 'addTorrent', selectedPath) - }) - }) -} - -// Prompts the user for the URL of a torrent file, then downloads and adds it -function showOpenTorrentAddress () { - windows.main.send('showOpenTorrentAddress') -} - function getAppMenuTemplate () { var template = [ { @@ -234,17 +164,17 @@ function getAppMenuTemplate () { ? 'Create New Torrent...' : 'Create New Torrent from Folder...', accelerator: 'CmdOrCtrl+N', - click: showOpenSeedFiles + click: () => dialog.openSeedDirectory() }, { label: 'Open Torrent File...', accelerator: 'CmdOrCtrl+O', - click: showOpenTorrentFile + click: () => dialog.openTorrentFile() }, { label: 'Open Torrent Address...', accelerator: 'CmdOrCtrl+U', - click: showOpenTorrentAddress + click: () => dialog.openTorrentAddress() }, { type: 'separator' @@ -300,7 +230,7 @@ function getAppMenuTemplate () { accelerator: process.platform === 'darwin' ? 'Ctrl+Command+F' : 'F11', - click: () => toggleFullScreen() + click: () => windows.toggleFullScreen() }, { label: 'Float on Top', @@ -505,7 +435,7 @@ function getAppMenuTemplate () { // File menu (Windows, Linux) template[0].submenu.unshift({ label: 'Create New Torrent from File...', - click: showOpenSeedFile + click: () => dialog.openSeedFile() }) // Help menu (Windows, Linux) @@ -515,7 +445,7 @@ function getAppMenuTemplate () { }, { label: 'About ' + config.APP_NAME, - click: windows.createAboutWindow + click: () => windows.about.create() } ) } @@ -537,17 +467,17 @@ function getDockMenuTemplate () { { label: 'Create New Torrent...', accelerator: 'CmdOrCtrl+N', - click: showOpenSeedFiles + click: () => dialog.openSeedDirectory() }, { label: 'Open Torrent File...', accelerator: 'CmdOrCtrl+O', - click: showOpenTorrentFile + click: () => dialog.openTorrentFile() }, { label: 'Open Torrent Address...', accelerator: 'CmdOrCtrl+U', - click: showOpenTorrentAddress + click: () => dialog.openTorrentAddress() } ] } diff --git a/main/power-save-blocker.js b/main/power-save-blocker.js new file mode 100644 index 00000000..9511ccf1 --- /dev/null +++ b/main/power-save-blocker.js @@ -0,0 +1,25 @@ +module.exports = { + start, + stop +} + +var electron = require('electron') +var log = require('./log') + +var powerSaveBlockerId = 0 + +function start () { + // Stop the previous power saver block, if one exists. + stop() + + powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep') + log('powerSaveBlocker.start %d', powerSaveBlockerId) +} + +function stop () { + if (!electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) { + return + } + electron.powerSaveBlocker.stop(powerSaveBlockerId) + log('powerSaveBlocker.stop %d', powerSaveBlockerId) +} diff --git a/main/tray.js b/main/tray.js index f0b40a99..a0cb2d1d 100644 --- a/main/tray.js +++ b/main/tray.js @@ -1,9 +1,10 @@ module.exports = { + hasTray, init, - hasTray + onWindowHide, + onWindowShow } -var cp = require('child_process') var electron = require('electron') var app = electron.app @@ -11,41 +12,32 @@ var app = electron.app var config = require('../config') var windows = require('./windows') -var trayIcon +var tray function init () { - // OS X has no tray icon - if (process.platform === 'darwin') return - - // On Linux, asynchronously check for libappindicator1 if (process.platform === 'linux') { - checkLinuxTraySupport(function (supportsTray) { - if (supportsTray) createTrayIcon() - }) + initLinux() } - - // Windows always supports minimize-to-tray - if (process.platform === 'win32') createTrayIcon() + if (process.platform === 'win32') { + initWin32() + } + // OS X apps generally do not have menu bar icons } -function hasTray () { - return !!trayIcon +function initLinux () { + // Check for libappindicator1 support before creating tray icon + checkLinuxTraySupport(function (supportsTray) { + if (supportsTray) createTray() + }) } -function createTrayIcon () { - trayIcon = new electron.Tray(getIconPath()) - - // On Windows, left click to open the app, right click for context menu - // On Linux, any click (right or left) opens the context menu - trayIcon.on('click', showApp) - - // Show the tray context menu, and keep the available commands up to date - updateTrayMenu() - windows.main.on('show', updateTrayMenu) - windows.main.on('hide', updateTrayMenu) +function initWin32 () { + createTray() } function checkLinuxTraySupport (cb) { + var cp = require('child_process') + // Check that we're on Ubuntu (or another debian system) and that we have // libappindicator1. If WebTorrent was installed from the deb file, we should // always have it. If it was installed from the zip file, we might not. @@ -57,18 +49,54 @@ function checkLinuxTraySupport (cb) { }) } +function hasTray () { + return !!tray +} + +function createTray () { + tray = new electron.Tray(getIconPath()) + + // On Windows, left click opens the app, right click opens the context menu. + // On Linux, any click (left or right) opens the context menu. + tray.on('click', showApp) + + // Show the tray context menu, and keep the available commands up to date + updateTrayMenu() +} + +function onWindowHide () { + updateTrayMenu() +} + +function onWindowShow () { + updateTrayMenu() +} + function updateTrayMenu () { - var showHideMenuItem - if (windows.main.isVisible()) { - showHideMenuItem = { label: 'Hide to tray', click: hideApp } - } else { - showHideMenuItem = { label: 'Show', click: showApp } - } + if (!tray) return + var contextMenu = electron.Menu.buildFromTemplate([ - showHideMenuItem, - { label: 'Quit', click: () => app.quit() } + getToggleItem(), + { + label: 'Quit', + click: () => app.quit() + } ]) - trayIcon.setContextMenu(contextMenu) + tray.setContextMenu(contextMenu) + + function getToggleItem () { + if (windows.main.win.isVisible()) { + return { + label: 'Hide to tray', + click: hideApp + } + } else { + return { + label: 'Show WebTorrent', + click: showApp + } + } + } } function showApp () { diff --git a/main/windows.js b/main/windows.js deleted file mode 100644 index 62e15aa6..00000000 --- a/main/windows.js +++ /dev/null @@ -1,145 +0,0 @@ -var windows = module.exports = { - about: null, - main: null, - createAboutWindow, - createWebTorrentHiddenWindow, - createMainWindow, - focusWindow -} - -var electron = require('electron') - -var app = electron.app - -var config = require('../config') -var menu = require('./menu') -var tray = require('./tray') - -function createAboutWindow () { - if (windows.about) { - return focusWindow(windows.about) - } - var win = windows.about = new electron.BrowserWindow({ - backgroundColor: '#ECECEC', - show: false, - center: true, - resizable: false, - icon: getIconPath(), - title: process.platform !== 'darwin' - ? 'About ' + config.APP_WINDOW_TITLE - : '', - useContentSize: true, // Specify web page size without OS chrome - width: 300, - height: 170, - minimizable: false, - maximizable: false, - fullscreen: false, - skipTaskbar: true - }) - win.loadURL(config.WINDOW_ABOUT) - - // No window menu - win.setMenu(null) - - win.webContents.on('did-finish-load', function () { - win.show() - }) - - win.once('closed', function () { - windows.about = null - }) -} - -function createWebTorrentHiddenWindow () { - var win = windows.webtorrent = new electron.BrowserWindow({ - backgroundColor: '#1E1E1E', - show: false, - center: true, - title: 'webtorrent-hidden-window', - useContentSize: true, - width: 150, - height: 150, - minimizable: false, - maximizable: false, - resizable: false, - fullscreenable: false, - fullscreen: false, - skipTaskbar: true - }) - win.loadURL(config.WINDOW_WEBTORRENT) - - // Prevent killing the WebTorrent process - win.on('close', function (e) { - if (!app.isQuitting) { - e.preventDefault() - win.hide() - } - }) - - win.once('closed', function () { - windows.webtorrent = null - }) -} - -var HEADER_HEIGHT = 37 -var TORRENT_HEIGHT = 100 - -function createMainWindow () { - if (windows.main) { - return focusWindow(windows.main) - } - var win = windows.main = new electron.BrowserWindow({ - backgroundColor: '#1E1E1E', - darkTheme: true, // Forces dark theme (GTK+3) - icon: getIconPath(), // Window icon (Windows, Linux) - minWidth: config.WINDOW_MIN_WIDTH, - minHeight: config.WINDOW_MIN_HEIGHT, - show: false, // Hide window until renderer sends 'ipcReady' event - title: config.APP_WINDOW_TITLE, - titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X) - useContentSize: true, // Specify web page size without OS chrome - width: 500, - height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents - }) - win.loadURL(config.WINDOW_MAIN) - if (process.platform === 'darwin') { - win.setSheetOffset(HEADER_HEIGHT) - } - - win.webContents.on('dom-ready', function () { - menu.onToggleFullScreen() - }) - - win.on('blur', menu.onWindowHide) - win.on('focus', menu.onWindowShow) - - win.on('enter-full-screen', () => menu.onToggleFullScreen(true)) - win.on('leave-full-screen', () => menu.onToggleFullScreen(false)) - - win.on('close', function (e) { - if (process.platform !== 'darwin' && !tray.hasTray()) { - app.quit() - } else if (!app.isQuitting) { - e.preventDefault() - win.hide() - win.send('dispatch', 'backToList') - } - }) - - win.once('closed', function () { - windows.main = null - }) -} - -function focusWindow (win) { - if (win.isMinimized()) { - win.restore() - } - win.show() // shows and gives focus -} - -function getIconPath () { - return process.platform === 'win32' - ? config.APP_ICON + '.ico' - : config.APP_ICON + '.png' -} diff --git a/main/windows/about.js b/main/windows/about.js new file mode 100644 index 00000000..0022c6e9 --- /dev/null +++ b/main/windows/about.js @@ -0,0 +1,50 @@ +var about = module.exports = { + create, + win: null +} + +var electron = require('electron') + +var config = require('../../config') +var util = require('./util') + +function create () { + if (about.win) { + return util.focusWindow(about.win) + } + + var win = about.win = new electron.BrowserWindow({ + backgroundColor: '#ECECEC', + center: true, + fullscreen: false, + height: 170, + icon: getIconPath(), + maximizable: false, + minimizable: false, + resizable: false, + show: false, + skipTaskbar: true, + useContentSize: true, + width: 300 + }) + + win.loadURL(config.WINDOW_ABOUT) + + // No menu on the About window + win.setMenu(null) + + // TODO: can this be removed? + win.webContents.on('did-finish-load', function () { + win.show() + }) + + win.once('closed', function () { + about.win = null + }) +} + +function getIconPath () { + return process.platform === 'win32' + ? config.APP_ICON + '.ico' + : config.APP_ICON + '.png' +} diff --git a/main/windows/index.js b/main/windows/index.js new file mode 100644 index 00000000..bee0c1ec --- /dev/null +++ b/main/windows/index.js @@ -0,0 +1,3 @@ +exports.about = require('./about') +exports.main = require('./main') +exports.webtorrent = require('./webtorrent') diff --git a/main/windows/main.js b/main/windows/main.js new file mode 100644 index 00000000..29c52b06 --- /dev/null +++ b/main/windows/main.js @@ -0,0 +1,115 @@ +var main = module.exports = { + create, + focus, + hide, + send, + show, + toggleFullScreen, + win: null +} + +var electron = require('electron') + +var app = electron.app + +var config = require('../../config') +var log = require('../log') +var menu = require('../menu') +var tray = require('../tray') +var util = require('./util') + +var HEADER_HEIGHT = 37 +var TORRENT_HEIGHT = 100 + +function create () { + if (main.win) { + return util.focusWindow(main.win) + } + var win = main.win = new electron.BrowserWindow({ + backgroundColor: '#1E1E1E', + darkTheme: true, // Forces dark theme (GTK+3) + icon: getIconPath(), // Window icon (Windows, Linux) + minWidth: config.WINDOW_MIN_WIDTH, + minHeight: config.WINDOW_MIN_HEIGHT, + show: false, // Hide window until renderer sends 'ipcReady' + title: config.APP_WINDOW_TITLE, + titleBarStyle: 'hidden-inset', // Hide title bar (OS X) + useContentSize: true, // Specify web page size without OS chrome + width: 500, + height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents + }) + + win.loadURL(config.WINDOW_MAIN) + + if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT) + + win.webContents.on('dom-ready', function () { + menu.onToggleFullScreen() + }) + + win.on('blur', function () { + menu.onWindowHide() + tray.onWindowHide() + }) + + win.on('focus', function () { + menu.onWindowShow() + tray.onWindowShow() + }) + + win.on('enter-full-screen', () => menu.onToggleFullScreen(true)) + win.on('leave-full-screen', () => menu.onToggleFullScreen(false)) + + win.on('close', function (e) { + if (process.platform !== 'darwin' && !tray.hasTray()) { + app.quit() + } else if (!app.isQuitting) { + e.preventDefault() + win.hide() + win.send('dispatch', 'backToList') + } + }) +} + +function getIconPath () { + return process.platform === 'win32' + ? config.APP_ICON + '.ico' + : config.APP_ICON + '.png' +} + +function toggleFullScreen (flag) { + if (!main.win || !main.win.isVisible()) { + return + } + + if (flag == null) flag = !main.win.isFullScreen() + + log('toggleFullScreen %s', flag) + + if (flag) { + // Fullscreen behaves oddly unless the aspect ratio is disabled. (OS X) + main.win.setAspectRatio(0) + } + + main.win.setFullScreen(flag) +} + +function send (...args) { + if (!main.win) return + main.win.send(...args) +} + +function show () { + if (!main.win) return + main.win.show() +} + +function hide () { + if (!main.win) return + main.win.hide() +} + +function focus () { + if (!main.win) return + util.focusWindow(main.win) +} diff --git a/main/windows/util.js b/main/windows/util.js new file mode 100644 index 00000000..d927ed25 --- /dev/null +++ b/main/windows/util.js @@ -0,0 +1,11 @@ +module.exports = { + focusWindow +} + +function focusWindow (win) { + if (win.isMinimized()) { + // TODO: can this be removed? + win.restore() + } + win.show() // shows and gives focus +} diff --git a/main/windows/webtorrent.js b/main/windows/webtorrent.js new file mode 100644 index 00000000..7d6b66f4 --- /dev/null +++ b/main/windows/webtorrent.js @@ -0,0 +1,48 @@ +var webtorrent = module.exports = { + create, + send, + show, + win: null +} + +var config = require('../../config') +var electron = require('electron') + +function create () { + var win = webtorrent.win = new electron.BrowserWindow({ + backgroundColor: '#1E1E1E', + center: true, + fullscreen: false, + fullscreenable: false, + height: 150, + maximizable: false, + minimizable: false, + resizable: false, + show: false, + skipTaskbar: true, + title: 'webtorrent-hidden-window', + useContentSize: true, + width: 150 + }) + + win.loadURL(config.WINDOW_WEBTORRENT) + + // Prevent killing the WebTorrent process + win.on('close', function (e) { + if (electron.app.isQuitting) { + return + } + e.preventDefault() + win.hide() + }) +} + +function show () { + if (!webtorrent.win) return + webtorrent.win.show() +} + +function send (...args) { + if (!webtorrent.win) return + webtorrent.win.send(...args) +} diff --git a/renderer/main.js b/renderer/main.js index fe97964f..5c834024 100644 --- a/renderer/main.js +++ b/renderer/main.js @@ -606,7 +606,8 @@ function saveState () { update() } -// Called when the user drag-drops files onto the app +// 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 ] @@ -674,12 +675,13 @@ function addTorrent (torrentId) { } function addSubtitles (files, autoSelect) { - // Subtitles are only supported while playing video + // Subtitles are only supported when playing video files if (state.playing.type !== 'video') return + if (files.length === 0) return // Read the files concurrently, then add all resulting subtitle tracks - var jobs = files.map((file) => (cb) => loadSubtitle(file, cb)) - parallel(jobs, function (err, tracks) { + var tasks = files.map((file) => (cb) => loadSubtitle(file, cb)) + parallel(tasks, function (err, tracks) { if (err) return onError(err) for (var i = 0; i < tracks.length; i++) { From 8ae4ac47e6f434e7099c54a09a3109788b4bae81 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Fri, 27 May 2016 18:51:42 -0700 Subject: [PATCH 06/11] Perf: Temporarily disable dynamic subtitle detection For https://github.com/feross/webtorrent-desktop/pull/511#issuecomment-22119 8555 --- renderer/main.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/renderer/main.js b/renderer/main.js index 5c834024..0e8330b1 100644 --- a/renderer/main.js +++ b/renderer/main.js @@ -976,7 +976,10 @@ function torrentProgress (progressInfo) { torrentSummary.progress = p }) - checkForSubtitles() + // TODO: Find an efficient way to re-enable this line, which allows subtitle + // files which are completed after a video starts to play to be added + // dynamically to the list of subtitles. + // checkForSubtitles() update() } From 5767d5b95d455722f562286e973b97f10d2486a3 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Sat, 28 May 2016 18:24:58 -0700 Subject: [PATCH 07/11] re-order view menu --- main/menu.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/main/menu.js b/main/menu.js index 730383d1..0c7c3d4c 100644 --- a/main/menu.js +++ b/main/menu.js @@ -240,6 +240,14 @@ function getAppMenuTemplate () { { type: 'separator' }, + { + label: 'Go Back', + accelerator: 'Esc', + click: escapeBack + }, + { + type: 'separator' + }, { label: 'Developer', submenu: [ @@ -258,14 +266,6 @@ function getAppMenuTemplate () { click: showWebTorrentWindow } ] - }, - { - type: 'separator' - }, - { - label: 'Go Back', - accelerator: 'Esc', - click: escapeBack } ] }, From 8b773c5f592f0b0355a847952ab0b6dd0d7e58f8 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Sat, 28 May 2016 18:25:25 -0700 Subject: [PATCH 08/11] Document and cleanup announcement/dialog/handlers.js --- main/announcement.js | 21 ++++++++++++++++++--- main/dialog.js | 36 +++++++++++++++++++++++------------- main/handlers.js | 33 +++++++++++++++++++++++---------- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/main/announcement.js b/main/announcement.js index b6977533..275bc0de 100644 --- a/main/announcement.js +++ b/main/announcement.js @@ -11,21 +11,36 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL + '?version=' + config.APP_VERSION + '&platform=' + process.platform +/** + * In certain situations, the WebTorrent team may need to show an announcement to + * all WebTorrent Desktop users. For example: a security notice, or an update + * notification (if the auto-updater stops working). + * + * When there is an announcement, the `ANNOUNCEMENT_URL` endpoint should return an + * HTTP 200 status code with a JSON object like this: + * + * { + * "title": "WebTorrent Desktop Announcement", + * "message": "Security Issue in v0.xx", + * "detail": "Please update to v0.xx as soon as possible..." + * } + */ function init () { var get = require('simple-get') get.concat(ANNOUNCEMENT_URL, onResponse) } function onResponse (err, res, data) { - if (err) return log('failed to retrieve remote message') - if (res.statusCode !== 200) return log('no remote message') + if (err) return log(`Failed to retrieve announcement: ${err.message}`) + if (res.statusCode !== 200) return log('No announcement exists') try { data = JSON.parse(data.toString()) } catch (err) { + // Support plaintext announcement messages, using a default title. data = { title: 'WebTorrent Desktop Announcement', - message: 'Announcement', + message: data.toString(), detail: data.toString() } } diff --git a/main/dialog.js b/main/dialog.js index 1aaab35f..585dc68f 100644 --- a/main/dialog.js +++ b/main/dialog.js @@ -8,38 +8,46 @@ module.exports = { var electron = require('electron') var windows = require('./windows') -// Prompts the user for a file, then creates a torrent. Only allows a single file -// selection. +/** + * Show open dialog to create a single-file torrent. + */ function openSeedFile () { - electron.dialog.showOpenDialog({ + var opts = { title: 'Select a file for the torrent file.', properties: [ 'openFile' ] - }, function (selectedPaths) { + } + electron.dialog.showOpenDialog(opts, function (selectedPaths) { if (!Array.isArray(selectedPaths)) return windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) }) } -// Prompts the user for a file or directory, then creates a torrent. Only allows a -// single selection. To create a multi-file torrent, the user must select a -// directory. +/* + * Show open dialog to create a single-file or single-directory torrent. On + * Windows and Linux, open dialogs are for files *or* directories only, not both. + * This function shows a directory dialog. + */ function openSeedDirectory () { - electron.dialog.showOpenDialog({ + var opts = { title: 'Select a file or folder for the torrent file.', properties: [ 'openFile', 'openDirectory' ] - }, function (selectedPaths) { + } + electron.dialog.showOpenDialog(opts, function (selectedPaths) { if (!Array.isArray(selectedPaths)) return windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) }) } -// Prompts the user to choose a torrent file, then adds it. +/* + * Show open dialog to open a .torrent file. + */ function openTorrentFile () { - electron.dialog.showOpenDialog(windows.main.win, { + var opts = { title: 'Select a .torrent file to open.', filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], properties: [ 'openFile', 'multiSelections' ] - }, function (selectedPaths) { + } + electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { if (!Array.isArray(selectedPaths)) return selectedPaths.forEach(function (selectedPath) { windows.main.send('dispatch', 'addTorrent', selectedPath) @@ -47,7 +55,9 @@ function openTorrentFile () { }) } -// Prompts the user for the URL of a torrent file, then downloads and adds it +/* + * Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.) + */ function openTorrentAddress () { windows.main.send('showOpenTorrentAddress') } diff --git a/main/handlers.js b/main/handlers.js index a9aab18a..33da88fe 100644 --- a/main/handlers.js +++ b/main/handlers.js @@ -3,9 +3,8 @@ module.exports = { uninstall } -var path = require('path') - var config = require('../config') +var path = require('path') function install () { if (process.platform === 'darwin') { @@ -35,11 +34,11 @@ function installDarwin () { var electron = require('electron') var app = electron.app - // On OS X, only protocols that are listed in Info.plist can be set as the default - // handler at runtime. + // On OS X, only protocols that are listed in `Info.plist` can be set as the + // default handler at runtime. app.setAsDefaultProtocolClient('magnet') - // File handlers are registered in the Info.plist. + // File handlers are defined in `Info.plist`. } function uninstallDarwin () {} @@ -55,10 +54,22 @@ function installWin32 () { var log = require('./log') - var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico') - - registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND) - registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND) + var iconPath = path.join( + process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico' + ) + registerProtocolHandlerWin32( + 'magnet', + 'URL:BitTorrent Magnet URL', + iconPath, + EXEC_COMMAND + ) + registerFileHandlerWin32( + '.torrent', + 'io.webtorrent.torrent', + 'BitTorrent Document', + iconPath, + EXEC_COMMAND + ) /** * To add a protocol handler, the following keys must be added to the Windows registry: @@ -265,7 +276,9 @@ function installLinux () { installIconFile() function installDesktopFile () { - var templatePath = path.join(config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop') + var templatePath = path.join( + config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop' + ) fs.readFile(templatePath, 'utf8', writeDesktopFile) } From 7833f6bbc455839126082c3abba1b03c51cf85bd Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Sat, 28 May 2016 19:09:05 -0700 Subject: [PATCH 09/11] Dialogs on do not show a title on OS X, so the window title is used instead. --- main/dialog.js | 46 +++++++++++++++++++++++++++++++++++++--------- renderer/main.js | 3 +++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/main/dialog.js b/main/dialog.js index 585dc68f..bf9d476f 100644 --- a/main/dialog.js +++ b/main/dialog.js @@ -5,6 +5,7 @@ module.exports = { openTorrentAddress } +var config = require('../config') var electron = require('electron') var windows = require('./windows') @@ -12,11 +13,14 @@ var windows = require('./windows') * Show open dialog to create a single-file torrent. */ function openSeedFile () { + if (!windows.main.win) return var opts = { - title: 'Select a file for the torrent file.', + title: 'Select a file for the torrent.', properties: [ 'openFile' ] } - electron.dialog.showOpenDialog(opts, function (selectedPaths) { + setTitle(opts.title) + electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { + resetTitle() if (!Array.isArray(selectedPaths)) return windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) }) @@ -24,15 +28,23 @@ function openSeedFile () { /* * Show open dialog to create a single-file or single-directory torrent. On - * Windows and Linux, open dialogs are for files *or* directories only, not both. - * This function shows a directory dialog. + * Windows and Linux, open dialogs are for files *or* directories only, not both, + * so this function shows a directory dialog on those platforms. */ function openSeedDirectory () { - var opts = { - title: 'Select a file or folder for the torrent file.', - properties: [ 'openFile', 'openDirectory' ] - } - electron.dialog.showOpenDialog(opts, function (selectedPaths) { + if (!windows.main.win) return + var opts = process.platform === 'darwin' + ? { + title: 'Select a file or folder for the torrent.', + properties: [ 'openFile', 'openDirectory' ] + } + : { + title: 'Select a folder for the torrent.', + properties: [ 'openDirectory' ] + } + setTitle(opts.title) + electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { + resetTitle() if (!Array.isArray(selectedPaths)) return windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) }) @@ -42,12 +54,15 @@ function openSeedDirectory () { * Show open dialog to open a .torrent file. */ function openTorrentFile () { + if (!windows.main.win) return var opts = { title: 'Select a .torrent file to open.', filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], properties: [ 'openFile', 'multiSelections' ] } + setTitle(opts.title) electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { + resetTitle() if (!Array.isArray(selectedPaths)) return selectedPaths.forEach(function (selectedPath) { windows.main.send('dispatch', 'addTorrent', selectedPath) @@ -61,3 +76,16 @@ function openTorrentFile () { function openTorrentAddress () { windows.main.send('showOpenTorrentAddress') } + +/** + * Dialogs on do not show a title on OS X, so the window title is used instead. + */ +function setTitle (title) { + if (process.platform === 'darwin') { + windows.main.send('dispatch', 'setTitle', title) + } +} + +function resetTitle () { + setTitle(config.APP_WINDOW_TITLE) +} diff --git a/renderer/main.js b/renderer/main.js index 0e8330b1..19ad69fd 100644 --- a/renderer/main.js +++ b/renderer/main.js @@ -377,6 +377,9 @@ function dispatch (action, ...args) { if (action === 'saveState') { saveState() } + if (action === 'setTitle') { + state.window.title = args[0] /* title */ + } // Update the virtual-dom, unless it's just a mouse move event if (action !== 'mediaMouseMoved' || showOrHidePlayerControls()) { From d4efebd694c88b1685db8462fe1f2fdf4c21cdf9 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Sat, 28 May 2016 19:10:55 -0700 Subject: [PATCH 10/11] Remove focusWindow function Not needed -- win.show() automatically focuses, unminimizes, and shows the window. --- main/index.js | 4 ++-- main/ipc.js | 5 +---- main/windows/about.js | 6 ++---- main/windows/main.js | 7 +------ main/windows/util.js | 11 ----------- renderer/main.js | 2 +- 6 files changed, 7 insertions(+), 28 deletions(-) delete mode 100644 main/windows/util.js diff --git a/main/index.js b/main/index.js index 79ad6389..eebdc44d 100644 --- a/main/index.js +++ b/main/index.js @@ -106,7 +106,7 @@ function onOpen (e, torrentId) { // The confirmation dialog Chrome shows causes Chrome to steal back the focus. // Electron issue: https://github.com/atom/electron/issues/4338 setTimeout(function () { - windows.main.focus() + windows.main.show() }, 100) } else { argv.push(torrentId) @@ -119,7 +119,7 @@ function onAppOpen (newArgv) { if (app.ipcReady) { log('Second app instance opened, but was prevented:', newArgv) - windows.main.focus() + windows.main.show() processArgv(newArgv) } else { diff --git a/main/ipc.js b/main/ipc.js index 6498f21e..c03c9cb0 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -65,6 +65,7 @@ function init () { }) ipcMain.on('openItem', function (e, path) { + ipc.on('show', (e, ...args) => windows.main.show(...args)) log('open item: ' + path) electron.shell.openItem(path) }) @@ -87,10 +88,6 @@ function init () { shortcuts.onPlayerOpen() }) - ipcMain.on('focusWindow', function (e, windowName) { - windows.focusWindow(windows[windowName]) - }) - ipcMain.on('downloadFinished', function (e, filePath) { if (app.dock) { // Bounces the Downloads stack if the filePath is inside the Downloads folder. diff --git a/main/windows/about.js b/main/windows/about.js index 0022c6e9..31e27f69 100644 --- a/main/windows/about.js +++ b/main/windows/about.js @@ -3,14 +3,12 @@ var about = module.exports = { win: null } -var electron = require('electron') - var config = require('../../config') -var util = require('./util') +var electron = require('electron') function create () { if (about.win) { - return util.focusWindow(about.win) + return about.win.show() } var win = about.win = new electron.BrowserWindow({ diff --git a/main/windows/main.js b/main/windows/main.js index 29c52b06..c60251e5 100644 --- a/main/windows/main.js +++ b/main/windows/main.js @@ -1,6 +1,5 @@ var main = module.exports = { create, - focus, hide, send, show, @@ -16,14 +15,13 @@ var config = require('../../config') var log = require('../log') var menu = require('../menu') var tray = require('../tray') -var util = require('./util') var HEADER_HEIGHT = 37 var TORRENT_HEIGHT = 100 function create () { if (main.win) { - return util.focusWindow(main.win) + return main.win.show() } var win = main.win = new electron.BrowserWindow({ backgroundColor: '#1E1E1E', @@ -109,7 +107,4 @@ function hide () { main.win.hide() } -function focus () { - if (!main.win) return - util.focusWindow(main.win) } diff --git a/main/windows/util.js b/main/windows/util.js deleted file mode 100644 index d927ed25..00000000 --- a/main/windows/util.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - focusWindow -} - -function focusWindow (win) { - if (win.isMinimized()) { - // TODO: can this be removed? - win.restore() - } - win.show() // shows and gives focus -} diff --git a/renderer/main.js b/renderer/main.js index 19ad69fd..3e7c4b53 100644 --- a/renderer/main.js +++ b/renderer/main.js @@ -1320,7 +1320,7 @@ function showDoneNotification (torrent) { }) notif.onclick = function () { - ipcRenderer.send('focusWindow', 'main') + ipcRenderer.send('show') } sound.play('DONE') From 62cb304971cb867e5923044df9b7afa2c5f35e78 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Sun, 29 May 2016 01:09:42 -0700 Subject: [PATCH 11/11] Move unrelated code out of menu.js and ipc.js --- main/dialog.js | 18 ++- main/dock.js | 59 ++++++++++ main/index.js | 16 +-- main/ipc.js | 187 +++++++++++-------------------- main/menu.js | 220 ++++++++++--------------------------- main/power-save-blocker.js | 23 ++-- main/shell.js | 32 ++++++ main/shortcuts.js | 5 +- main/squirrel-win32.js | 10 +- main/tray.js | 62 +++++------ main/updater.js | 34 +++--- main/windows/about.js | 4 +- main/windows/main.js | 167 ++++++++++++++++++++++------ main/windows/webtorrent.js | 20 +++- renderer/main.js | 13 +-- renderer/views/header.js | 2 +- 16 files changed, 469 insertions(+), 403 deletions(-) create mode 100644 main/dock.js create mode 100644 main/shell.js diff --git a/main/dialog.js b/main/dialog.js index bf9d476f..6fe06ab1 100644 --- a/main/dialog.js +++ b/main/dialog.js @@ -5,8 +5,10 @@ module.exports = { openTorrentAddress } -var config = require('../config') var electron = require('electron') + +var config = require('../config') +var log = require('./log') var windows = require('./windows') /** @@ -14,6 +16,7 @@ var windows = require('./windows') */ function openSeedFile () { if (!windows.main.win) return + log('openSeedFile') var opts = { title: 'Select a file for the torrent.', properties: [ 'openFile' ] @@ -22,7 +25,7 @@ function openSeedFile () { electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { resetTitle() if (!Array.isArray(selectedPaths)) return - windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) + windows.main.dispatch('showCreateTorrent', selectedPaths) }) } @@ -33,6 +36,7 @@ function openSeedFile () { */ function openSeedDirectory () { if (!windows.main.win) return + log('openSeedDirectory') var opts = process.platform === 'darwin' ? { title: 'Select a file or folder for the torrent.', @@ -46,7 +50,7 @@ function openSeedDirectory () { electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { resetTitle() if (!Array.isArray(selectedPaths)) return - windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) + windows.main.dispatch('showCreateTorrent', selectedPaths) }) } @@ -55,6 +59,7 @@ function openSeedDirectory () { */ function openTorrentFile () { if (!windows.main.win) return + log('openTorrentFile') var opts = { title: 'Select a .torrent file to open.', filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], @@ -65,7 +70,7 @@ function openTorrentFile () { resetTitle() if (!Array.isArray(selectedPaths)) return selectedPaths.forEach(function (selectedPath) { - windows.main.send('dispatch', 'addTorrent', selectedPath) + windows.main.dispatch('addTorrent', selectedPath) }) }) } @@ -74,7 +79,8 @@ function openTorrentFile () { * Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.) */ function openTorrentAddress () { - windows.main.send('showOpenTorrentAddress') + log('openTorrentAddress') + windows.main.dispatch('openTorrentAddress') } /** @@ -82,7 +88,7 @@ function openTorrentAddress () { */ function setTitle (title) { if (process.platform === 'darwin') { - windows.main.send('dispatch', 'setTitle', title) + windows.main.dispatch('setTitle', title) } } diff --git a/main/dock.js b/main/dock.js new file mode 100644 index 00000000..2d17a296 --- /dev/null +++ b/main/dock.js @@ -0,0 +1,59 @@ +module.exports = { + downloadFinished, + init, + setBadge +} + +var electron = require('electron') + +var app = electron.app + +var dialog = require('./dialog') +var log = require('./log') + +/** + * Add a right-click menu to the dock icon. (OS X) + */ +function init () { + if (!app.dock) return + var menu = electron.Menu.buildFromTemplate(getMenuTemplate()) + app.dock.setMenu(menu) +} + +/** + * Bounce the Downloads stack if `path` is inside the Downloads folder. (OS X) + */ +function downloadFinished (path) { + if (!app.dock) return + log(`downloadFinished: ${path}`) + app.dock.downloadFinished(path) +} + +/** + * Display string in dock badging area. (OS X) + */ +function setBadge (text) { + if (!app.dock) return + log(`setBadge: ${text}`) + app.dock.setBadge(String(text)) +} + +function getMenuTemplate () { + return [ + { + label: 'Create New Torrent...', + accelerator: 'CmdOrCtrl+N', + click: () => dialog.openSeedDirectory() + }, + { + label: 'Open Torrent File...', + accelerator: 'CmdOrCtrl+O', + click: () => dialog.openTorrentFile() + }, + { + label: 'Open Torrent Address...', + accelerator: 'CmdOrCtrl+U', + click: () => dialog.openTorrentAddress() + } + ] +} diff --git a/main/index.js b/main/index.js index eebdc44d..2d2c8b93 100644 --- a/main/index.js +++ b/main/index.js @@ -9,6 +9,7 @@ var announcement = require('./announcement') 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') @@ -61,8 +62,8 @@ function init () { app.on('ready', function () { isReady = true - windows.main.create() - windows.webtorrent.create() + windows.main.init() + windows.webtorrent.init() menu.init() // To keep app startup fast, some code is delayed. @@ -80,20 +81,21 @@ function init () { app.isQuitting = true e.preventDefault() - windows.main.send('dispatch', 'saveState') // try to save state on exit + windows.main.dispatch('saveState') // try to save state on exit ipcMain.once('savedState', () => app.quit()) setTimeout(() => app.quit(), 2000) // quit after 2 secs, at most }) app.on('activate', function () { - if (isReady) windows.main.create() + if (isReady) windows.main.show() }) } function delayedInit () { announcement.init() - tray.init() + dock.init() handlers.install() + tray.init() updater.init() } @@ -101,7 +103,7 @@ function onOpen (e, torrentId) { e.preventDefault() if (app.ipcReady) { - windows.main.send('dispatch', 'onOpen', torrentId) + windows.main.dispatch('onOpen', torrentId) // Magnet links opened from Chrome won't focus the app without a setTimeout. // The confirmation dialog Chrome shows causes Chrome to steal back the focus. // Electron issue: https://github.com/atom/electron/issues/4338 @@ -148,6 +150,6 @@ function processArgv (argv) { } }) if (paths.length > 0) { - windows.main.send('dispatch', 'onOpen', paths) + windows.main.dispatch('onOpen', paths) } } diff --git a/main/ipc.js b/main/ipc.js index c03c9cb0..b9c4c59a 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -6,9 +6,12 @@ var electron = require('electron') var app = electron.app +var dialog = require('./dialog') +var dock = require('./dock') var log = require('./log') var menu = require('./menu') var powerSaveBlocker = require('./power-save-blocker') +var shell = require('./shell') var shortcuts = require('./shortcuts') var vlc = require('./vlc') var windows = require('./windows') @@ -20,15 +23,15 @@ var messageQueueMainToWebTorrent = [] var vlcProcess function init () { - var ipcMain = electron.ipcMain + var ipc = electron.ipcMain - ipcMain.on('ipcReady', function (e) { + ipc.on('ipcReady', function (e) { windows.main.show() app.ipcReady = true app.emit('ipcReady') }) - ipcMain.on('ipcReadyWebTorrent', function (e) { + ipc.on('ipcReadyWebTorrent', function (e) { app.ipcReadyWebTorrent = true log('sending %d queued messages from the main win to the webtorrent window', messageQueueMainToWebTorrent.length) @@ -38,109 +41,111 @@ function init () { }) }) - ipcMain.on('showOpenTorrentFile', () => menu.showOpenTorrentFile()) + /** + * Dialog + */ - ipcMain.on('setBounds', function (e, bounds, maximize) { - setBounds(bounds, maximize) - }) + ipc.on('openTorrentFile', () => dialog.openTorrentFile()) - ipcMain.on('setAspectRatio', function (e, aspectRatio) { - setAspectRatio(aspectRatio) - }) + /** + * Dock + */ - ipcMain.on('setBadge', function (e, text) { - setBadge(text) - }) + ipc.on('setBadge', (e, ...args) => dock.setBadge(...args)) + ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args)) - ipcMain.on('setProgress', function (e, progress) { - setProgress(progress) - }) + /** + * Events + */ - ipcMain.on('toggleFullScreen', function (e, flag) { - windows.main.toggleFullScreen(flag) - }) - - ipcMain.on('setTitle', function (e, title) { - windows.main.win.setTitle(title) - }) - - ipcMain.on('openItem', function (e, path) { - ipc.on('show', (e, ...args) => windows.main.show(...args)) - log('open item: ' + path) - electron.shell.openItem(path) - }) - - ipcMain.on('showItemInFolder', function (e, path) { - log('show item in folder: ' + path) - electron.shell.showItemInFolder(path) - }) - - ipcMain.on('blockPowerSave', () => powerSaveBlocker.start()) - ipcMain.on('unblockPowerSave', () => powerSaveBlocker.stop()) - - ipcMain.on('onPlayerOpen', function () { + ipc.on('onPlayerOpen', function () { menu.onPlayerOpen() shortcuts.onPlayerOpen() }) - ipcMain.on('onPlayerClose', function () { + ipc.on('onPlayerClose', function () { menu.onPlayerClose() shortcuts.onPlayerOpen() }) - ipcMain.on('downloadFinished', function (e, filePath) { - if (app.dock) { - // Bounces the Downloads stack if the filePath is inside the Downloads folder. - app.dock.downloadFinished(filePath) - } - }) + /** + * Power Save Blocker + */ - ipcMain.on('checkForVLC', function (e) { + ipc.on('blockPowerSave', () => powerSaveBlocker.start()) + ipc.on('unblockPowerSave', () => powerSaveBlocker.stop()) + + /** + * Shell + */ + + ipc.on('openItem', (e, ...args) => shell.openItem(...args)) + ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args)) + + /** + * Windows: Main + */ + + var main = windows.main + + ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args)) + ipc.on('setBounds', (e, ...args) => main.setBounds(...args)) + ipc.on('setProgress', (e, ...args) => main.setProgress(...args)) + ipc.on('setTitle', (e, ...args) => main.setTitle(...args)) + ipc.on('show', () => main.show()) + ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args)) + + /** + * VLC + * TODO: Move most of this code to vlc.js + */ + + ipc.on('checkForVLC', function (e) { vlc.checkForVLC(function (isInstalled) { windows.main.send('checkForVLC', isInstalled) }) }) - ipcMain.on('vlcPlay', function (e, url) { + ipc.on('vlcPlay', function (e, url) { var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url] - console.log('Running vlc ' + args.join(' ')) + log('Running vlc ' + args.join(' ')) vlc.spawn(args, function (err, proc) { - if (err) return windows.main.send('dispatch', 'vlcNotFound') + if (err) return windows.main.dispatch('vlcNotFound') vlcProcess = proc // If it works, close the modal after a second var closeModalTimeout = setTimeout(() => - windows.main.send('dispatch', 'exitModal'), 1000) + windows.main.dispatch('exitModal'), 1000) vlcProcess.on('close', function (code) { clearTimeout(closeModalTimeout) if (!vlcProcess) return // Killed - console.log('VLC exited with code ', code) + log('VLC exited with code ', code) if (code === 0) { - windows.main.send('dispatch', 'backToList') + windows.main.dispatch('backToList') } else { - windows.main.send('dispatch', 'vlcNotFound') + windows.main.dispatch('vlcNotFound') } vlcProcess = null }) vlcProcess.on('error', function (e) { - console.log('VLC error', e) + log('VLC error', e) }) }) }) - ipcMain.on('vlcQuit', function () { + ipc.on('vlcQuit', function () { if (!vlcProcess) return - console.log('Killing VLC, pid ' + vlcProcess.pid) + log('Killing VLC, pid ' + vlcProcess.pid) vlcProcess.kill('SIGKILL') // kill -9 vlcProcess = null }) // Capture all events - var oldEmit = ipcMain.emit - ipcMain.emit = function (name, e, ...args) { + var oldEmit = ipc.emit + ipc.emit = function (name, e, ...args) { // Relay messages between the main window and the WebTorrent hidden window if (name.startsWith('wt-') && !app.isQuitting) { if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') { @@ -163,68 +168,6 @@ function init () { } // Emit all other events normally - oldEmit.call(ipcMain, name, e, ...args) + oldEmit.call(ipc, name, e, ...args) } } - -function setBounds (bounds, maximize) { - // Do nothing in fullscreen - if (!windows.main.win || windows.main.win.isFullScreen()) { - log('setBounds: not setting bounds because we\'re in full screen') - return - } - - // Maximize or minimize, if the second argument is present - var willBeMaximized - if (maximize === true) { - if (!windows.main.win.isMaximized()) { - log('setBounds: maximizing') - windows.main.win.maximize() - } - willBeMaximized = true - } else if (maximize === false) { - if (windows.main.win.isMaximized()) { - log('setBounds: unmaximizing') - windows.main.win.unmaximize() - } - willBeMaximized = false - } else { - willBeMaximized = windows.main.win.isMaximized() - } - - // Assuming we're not maximized or maximizing, set the window size - if (!willBeMaximized) { - log('setBounds: setting bounds to ' + JSON.stringify(bounds)) - if (bounds.x === null && bounds.y === null) { - // X and Y not specified? By default, center on current screen - var scr = electron.screen.getDisplayMatching(windows.main.win.getBounds()) - bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2) - bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2) - log('setBounds: centered to ' + JSON.stringify(bounds)) - } - windows.main.win.setBounds(bounds, true) - } else { - log('setBounds: not setting bounds because of window maximization') - } -} - -function setAspectRatio (aspectRatio) { - log('setAspectRatio %o', aspectRatio) - if (!windows.main.win) return - windows.main.win.setAspectRatio(aspectRatio) -} - -// Display string in dock badging area (OS X) -function setBadge (text) { - log('setBadge %s', text) - if (app.dock) { - app.dock.setBadge(String(text)) - } -} - -// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1. -function setProgress (progress) { - log('setProgress %s', progress) - if (!windows.main.win) return - windows.main.win.setProgressBar(progress) -} diff --git a/main/menu.js b/main/menu.js index 0c7c3d4c..640c4eba 100644 --- a/main/menu.js +++ b/main/menu.js @@ -2,9 +2,10 @@ module.exports = { init, onPlayerClose, onPlayerOpen, + onToggleAlwaysOnTop, onToggleFullScreen, - onWindowHide, - onWindowShow + onWindowBlur, + onWindowFocus } var electron = require('electron') @@ -13,148 +14,66 @@ var app = electron.app var config = require('../config') var dialog = require('./dialog') -var log = require('./log') +var shell = require('./shell') var windows = require('./windows') -var appMenu +var menu function init () { - appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate()) - electron.Menu.setApplicationMenu(appMenu) - - if (app.dock) { - var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate()) - app.dock.setMenu(dockMenu) - } -} - -// Sets whether the window should always show on top of other windows -function toggleFloatOnTop (flag) { - if (!windows.main.win) return - log('toggleFloatOnTop %s', flag) - flag = flag != null ? flag : !windows.main.isAlwaysOnTop() - windows.main.setAlwaysOnTop(flag) - getMenuItem('Float on Top').checked = flag -} - -function toggleDevTools () { - if (!windows.main.win) return - log('toggleDevTools') - windows.main.toggleDevTools() -} - -function showWebTorrentWindow () { - log('showWebTorrentWindow') - windows.webtorrent.show() - windows.webtorrent.win.webContents.openDevTools({ detach: true }) -} - -function playPause () { - if (!windows.main.win) return - windows.main.send('dispatch', 'playPause') -} - -function increaseVolume () { - if (!windows.main.win) return - windows.main.send('dispatch', 'changeVolume', 0.1) -} - -function decreaseVolume () { - if (!windows.main.win) return - windows.main.send('dispatch', 'changeVolume', -0.1) -} - -function openSubtitles () { - if (!windows.main.win) return - windows.main.send('dispatch', 'openSubtitles') -} - -function skipForward () { - if (!windows.main.win) return - windows.main.send('dispatch', 'skip', 1) -} - -function skipBack () { - if (!windows.main.win) return - windows.main.send('dispatch', 'skip', -1) -} - -function increasePlaybackRate () { - if (!windows.main.win) return - windows.main.send('dispatch', 'changePlaybackRate', 1) -} - -function decreasePlaybackRate () { - if (!windows.main.win) return - windows.main.send('dispatch', 'changePlaybackRate', -1) -} - -// Open the preferences window -function showPreferences () { - if (!windows.main.win) return - windows.main.send('dispatch', 'preferences') -} - -function escapeBack () { - if (!windows.main.win) return - windows.main.send('dispatch', 'escapeBack') -} - -function onWindowShow () { - log('onWindowShow') - getMenuItem('Full Screen').enabled = true - getMenuItem('Float on Top').enabled = true -} - -function onWindowHide () { - log('onWindowHide') - getMenuItem('Full Screen').enabled = false - getMenuItem('Float on Top').enabled = false -} - -function onPlayerOpen () { - log('onPlayerOpen') - getMenuItem('Play/Pause').enabled = true - getMenuItem('Increase Volume').enabled = true - getMenuItem('Decrease Volume').enabled = true - getMenuItem('Add Subtitles File...').enabled = true - getMenuItem('Step Forward').enabled = true - getMenuItem('Step Backward').enabled = true - getMenuItem('Increase Speed').enabled = true - getMenuItem('Decrease Speed').enabled = true + menu = electron.Menu.buildFromTemplate(getMenuTemplate()) + electron.Menu.setApplicationMenu(menu) } function onPlayerClose () { - log('onPlayerClose') getMenuItem('Play/Pause').enabled = false getMenuItem('Increase Volume').enabled = false getMenuItem('Decrease Volume').enabled = false - getMenuItem('Add Subtitles File...').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 onToggleFullScreen (isFullScreen) { - if (isFullScreen == null) { - isFullScreen = windows.main.win.isFullScreen() - } - windows.main.win.setMenuBarVisibility(!isFullScreen) - getMenuItem('Full Screen').checked = isFullScreen - windows.main.send('fullscreenChanged', isFullScreen) +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 onToggleAlwaysOnTop (flag) { + getMenuItem('Float on Top').checked = flag +} + +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 < appMenu.items.length; i++) { - var menuItem = appMenu.items[i].submenu.items.find(function (item) { + for (var i = 0; i < menu.items.length; i++) { + var menuItem = menu.items[i].submenu.items.find(function (item) { return item.label === label }) if (menuItem) return menuItem } } -function getAppMenuTemplate () { +function getMenuTemplate () { var template = [ { label: 'File', @@ -217,7 +136,7 @@ function getAppMenuTemplate () { { label: 'Preferences', accelerator: 'CmdOrCtrl+,', - click: () => showPreferences() + click: () => windows.main.dispatch('preferences') } ] }, @@ -235,7 +154,7 @@ function getAppMenuTemplate () { { label: 'Float on Top', type: 'checkbox', - click: () => toggleFloatOnTop() + click: () => windows.toggleAlwaysOnTop() }, { type: 'separator' @@ -243,7 +162,7 @@ function getAppMenuTemplate () { { label: 'Go Back', accelerator: 'Esc', - click: escapeBack + click: () => windows.main.dispatch('escapeBack') }, { type: 'separator' @@ -256,14 +175,14 @@ function getAppMenuTemplate () { accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', - click: toggleDevTools + click: () => windows.main.toggleDevTools() }, { label: 'Show WebTorrent Process', accelerator: process.platform === 'darwin' ? 'Alt+Command+P' : 'Ctrl+Shift+P', - click: showWebTorrentWindow + click: () => windows.webtorrent.toggleDevTools() } ] } @@ -275,7 +194,7 @@ function getAppMenuTemplate () { { label: 'Play/Pause', accelerator: 'Space', - click: playPause, + click: () => windows.main.dispatch('playPause'), enabled: false }, { @@ -284,13 +203,13 @@ function getAppMenuTemplate () { { label: 'Increase Volume', accelerator: 'CmdOrCtrl+Up', - click: increaseVolume, + click: () => windows.main.dispatch('changeVolume', 0.1), enabled: false }, { label: 'Decrease Volume', accelerator: 'CmdOrCtrl+Down', - click: decreaseVolume, + click: () => windows.main.dispatch('changeVolume', -0.1), enabled: false }, { @@ -299,13 +218,13 @@ function getAppMenuTemplate () { { label: 'Step Forward', accelerator: 'CmdOrCtrl+Alt+Right', - click: skipForward, + click: () => windows.main.dispatch('skip', 1), enabled: false }, { label: 'Step Backward', accelerator: 'CmdOrCtrl+Alt+Left', - click: skipBack, + click: () => windows.main.dispatch('skip', -1), enabled: false }, { @@ -314,13 +233,13 @@ function getAppMenuTemplate () { { label: 'Increase Speed', accelerator: 'CmdOrCtrl+=', - click: increasePlaybackRate, + click: () => windows.main.dispatch('changePlaybackRate', 1), enabled: false }, { label: 'Decrease Speed', accelerator: 'CmdOrCtrl+-', - click: decreasePlaybackRate, + click: () => windows.main.dispatch('changePlaybackRate', -1), enabled: false }, { @@ -328,7 +247,7 @@ function getAppMenuTemplate () { }, { label: 'Add Subtitles File...', - click: openSubtitles, + click: () => windows.main.dispatch('openSubtitles'), enabled: false } ] @@ -339,18 +258,18 @@ function getAppMenuTemplate () { submenu: [ { label: 'Learn more about ' + config.APP_NAME, - click: () => electron.shell.openExternal(config.HOME_PAGE_URL) + click: () => shell.openExternal(config.HOME_PAGE_URL) }, { label: 'Contribute on GitHub', - click: () => electron.shell.openExternal(config.GITHUB_URL) + click: () => shell.openExternal(config.GITHUB_URL) }, { type: 'separator' }, { label: 'Report an Issue...', - click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES) + click: () => shell.openExternal(config.GITHUB_URL_ISSUES) } ] } @@ -371,7 +290,7 @@ function getAppMenuTemplate () { { label: 'Preferences', accelerator: 'Cmd+,', - click: () => showPreferences() + click: () => windows.main.dispatch('preferences') }, { type: 'separator' @@ -430,7 +349,8 @@ function getAppMenuTemplate () { }) } - // In Linux and Windows it is not possible to open both folders and files + // On Windows and Linux, open dialogs do not support selecting both files and + // folders and files, so add an extra menu item so there is one for each type. if (process.platform === 'linux' || process.platform === 'win32') { // File menu (Windows, Linux) template[0].submenu.unshift({ @@ -445,12 +365,12 @@ function getAppMenuTemplate () { }, { label: 'About ' + config.APP_NAME, - click: () => windows.about.create() + click: () => windows.about.init() } ) } - // Add "File > Quit" menu item so Linux distros where the system tray icon is missing - // will have a way to quit the app. + // Add "File > Quit" menu item so Linux distros where the system tray icon is + // missing will have a way to quit the app. if (process.platform === 'linux') { // File menu (Linux) template[0].submenu.push({ @@ -461,23 +381,3 @@ function getAppMenuTemplate () { return template } - -function getDockMenuTemplate () { - return [ - { - label: 'Create New Torrent...', - accelerator: 'CmdOrCtrl+N', - click: () => dialog.openSeedDirectory() - }, - { - label: 'Open Torrent File...', - accelerator: 'CmdOrCtrl+O', - click: () => dialog.openTorrentFile() - }, - { - label: 'Open Torrent Address...', - accelerator: 'CmdOrCtrl+U', - click: () => dialog.openTorrentAddress() - } - ] -} diff --git a/main/power-save-blocker.js b/main/power-save-blocker.js index 9511ccf1..c7d76e42 100644 --- a/main/power-save-blocker.js +++ b/main/power-save-blocker.js @@ -6,20 +6,25 @@ module.exports = { var electron = require('electron') var log = require('./log') -var powerSaveBlockerId = 0 +var blockId = 0 +/** + * Block the system from entering low-power (sleep) mode or turning off the + * display. + */ function start () { - // Stop the previous power saver block, if one exists. - stop() - - powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep') - log('powerSaveBlocker.start %d', powerSaveBlockerId) + stop() // Stop the previous power saver block, if one exists. + blockId = electron.powerSaveBlocker.start('prevent-display-sleep') + log(`powerSaveBlocker.start: ${blockId}`) } +/** + * Stop blocking the system from entering low-power mode. + */ function stop () { - if (!electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) { + if (!electron.powerSaveBlocker.isStarted(blockId)) { return } - electron.powerSaveBlocker.stop(powerSaveBlockerId) - log('powerSaveBlocker.stop %d', powerSaveBlockerId) + electron.powerSaveBlocker.stop(blockId) + log(`powerSaveBlocker.stop: ${blockId}`) } diff --git a/main/shell.js b/main/shell.js new file mode 100644 index 00000000..8461d3ce --- /dev/null +++ b/main/shell.js @@ -0,0 +1,32 @@ +module.exports = { + openExternal, + openItem, + showItemInFolder +} + +var electron = require('electron') +var log = require('./log') + +/** + * Open the given external protocol URL in the desktop’s default manner. + */ +function openExternal (url) { + log(`openExternal: ${url}`) + electron.shell.openExternal(url) +} + +/** + * Open the given file in the desktop’s default manner. + */ +function openItem (path) { + log(`openItem: ${path}`) + electron.shell.openItem(path) +} + +/** + * Show the given file in a file manager. If possible, select the file. + */ +function showItemInFolder (path) { + log(`showItemInFolder: ${path}`) + electron.shell.showItemInFolder(path) +} diff --git a/main/shortcuts.js b/main/shortcuts.js index cfb212e9..4254fd37 100644 --- a/main/shortcuts.js +++ b/main/shortcuts.js @@ -7,13 +7,14 @@ var electron = require('electron') var windows = require('./windows') function onPlayerOpen () { - // Register special "media key" for play/pause, available on some keyboards + // Register play/pause media key, available on some keyboards. electron.globalShortcut.register( 'MediaPlayPause', - () => windows.main.send('dispatch', 'playPause') + () => windows.main.dispatch('playPause') ) } function onPlayerClose () { + // Return the media key to the OS, so other apps can use it. electron.globalShortcut.unregister('MediaPlayPause') } diff --git a/main/squirrel-win32.js b/main/squirrel-win32.js index f817a562..84087cfa 100644 --- a/main/squirrel-win32.js +++ b/main/squirrel-win32.js @@ -12,8 +12,8 @@ var app = electron.app var handlers = require('./handlers') -var exeName = path.basename(process.execPath) -var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe') +var EXE_NAME = path.basename(process.execPath) +var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe') function handleEvent (cmd) { if (cmd === '--squirrel-install') { @@ -102,12 +102,12 @@ function spawn (command, args, cb) { // Spawn Squirrel's Update.exe with the given arguments and invoke the callback when the // command completes. function spawnUpdate (args, cb) { - spawn(updateDotExe, args, cb) + spawn(UPDATE_EXE, args, cb) } // Create desktop/start menu shortcuts using the Squirrel Update.exe command line API function createShortcuts (cb) { - spawnUpdate(['--createShortcut', exeName], cb) + spawnUpdate(['--createShortcut', EXE_NAME], cb) } // Update desktop/start menu shortcuts using the Squirrel Update.exe command line API @@ -135,5 +135,5 @@ function updateShortcuts (cb) { // Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API function removeShortcuts (cb) { - spawnUpdate(['--removeShortcut', exeName], cb) + spawnUpdate(['--removeShortcut', EXE_NAME], cb) } diff --git a/main/tray.js b/main/tray.js index a0cb2d1d..73cd4ae2 100644 --- a/main/tray.js +++ b/main/tray.js @@ -1,8 +1,8 @@ module.exports = { hasTray, init, - onWindowHide, - onWindowShow + onWindowBlur, + onWindowFocus } var electron = require('electron') @@ -24,8 +24,24 @@ function init () { // OS X apps generally do not have menu bar icons } +/** + * Returns true if there a tray icon is active. + */ +function hasTray () { + return !!tray +} + +function onWindowBlur () { + if (!tray) return + updateTrayMenu() +} + +function onWindowFocus () { + if (!tray) return + updateTrayMenu() +} + function initLinux () { - // Check for libappindicator1 support before creating tray icon checkLinuxTraySupport(function (supportsTray) { if (supportsTray) createTray() }) @@ -35,6 +51,9 @@ function initWin32 () { createTray() } +/** + * Check for libappindicator1 support before creating tray icon + */ function checkLinuxTraySupport (cb) { var cp = require('child_process') @@ -49,65 +68,46 @@ function checkLinuxTraySupport (cb) { }) } -function hasTray () { - return !!tray -} - function createTray () { tray = new electron.Tray(getIconPath()) // On Windows, left click opens the app, right click opens the context menu. // On Linux, any click (left or right) opens the context menu. - tray.on('click', showApp) + tray.on('click', () => windows.main.show()) // Show the tray context menu, and keep the available commands up to date updateTrayMenu() } -function onWindowHide () { - updateTrayMenu() -} - -function onWindowShow () { - updateTrayMenu() -} - function updateTrayMenu () { - if (!tray) return + var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate) + tray.setContextMenu(contextMenu) +} - var contextMenu = electron.Menu.buildFromTemplate([ +function getMenuTemplate () { + return [ getToggleItem(), { label: 'Quit', click: () => app.quit() } - ]) - tray.setContextMenu(contextMenu) + ] function getToggleItem () { if (windows.main.win.isVisible()) { return { label: 'Hide to tray', - click: hideApp + click: () => windows.main.hide() } } else { return { label: 'Show WebTorrent', - click: showApp + click: () => windows.main.show() } } } } -function showApp () { - windows.main.show() -} - -function hideApp () { - windows.main.hide() - windows.main.send('dispatch', 'backToList') -} - function getIconPath () { return process.platform === 'win32' ? config.APP_ICON + '.ico' diff --git a/main/updater.js b/main/updater.js index 71f8922b..9fe30eb6 100644 --- a/main/updater.js +++ b/main/updater.js @@ -21,27 +21,27 @@ function init () { } } -// The Electron auto-updater does not support Linux yet, so manually check for updates and -// `show the user a modal notification. +// The Electron auto-updater does not support Linux yet, so manually check for +// updates and show the user a modal notification. function initLinux () { get.concat(AUTO_UPDATE_URL, onResponse) +} - function onResponse (err, res, data) { - if (err) return log(`Update error: ${err.message}`) - if (res.statusCode === 200) { - // Update available - try { - data = JSON.parse(data) - } catch (err) { - return log(`Update error: Invalid JSON response: ${err.message}`) - } - windows.main.send('dispatch', 'updateAvailable', data.version) - } else if (res.statusCode === 204) { - // No update available - } else { - // Unexpected status code - log(`Update error: Unexpected status code: ${res.statusCode}`) +function onResponse (err, res, data) { + if (err) return log(`Update error: ${err.message}`) + if (res.statusCode === 200) { + // Update available + try { + data = JSON.parse(data) + } catch (err) { + return log(`Update error: Invalid JSON response: ${err.message}`) } + windows.main.dispatch('updateAvailable', data.version) + } else if (res.statusCode === 204) { + // No update available + } else { + // Unexpected status code + log(`Update error: Unexpected status code: ${res.statusCode}`) } } diff --git a/main/windows/about.js b/main/windows/about.js index 31e27f69..7fba55d1 100644 --- a/main/windows/about.js +++ b/main/windows/about.js @@ -1,12 +1,12 @@ var about = module.exports = { - create, + init, win: null } var config = require('../../config') var electron = require('electron') -function create () { +function init () { if (about.win) { return about.win.show() } diff --git a/main/windows/main.js b/main/windows/main.js index c60251e5..f0d7a4ed 100644 --- a/main/windows/main.js +++ b/main/windows/main.js @@ -1,8 +1,15 @@ var main = module.exports = { - create, + dispatch, hide, + init, send, + setAspectRatio, + setBounds, + setProgress, + setTitle, show, + toggleAlwaysOnTop, + toggleDevTools, toggleFullScreen, win: null } @@ -19,7 +26,7 @@ var tray = require('../tray') var HEADER_HEIGHT = 37 var TORRENT_HEIGHT = 100 -function create () { +function init () { if (main.win) { return main.win.show() } @@ -42,21 +49,30 @@ function create () { if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT) win.webContents.on('dom-ready', function () { - menu.onToggleFullScreen() + menu.onToggleFullScreen(main.win.isFullScreen()) }) win.on('blur', function () { - menu.onWindowHide() - tray.onWindowHide() + menu.onWindowBlur() + tray.onWindowBlur() }) win.on('focus', function () { - menu.onWindowShow() - tray.onWindowShow() + menu.onWindowFocus() + tray.onWindowFocus() }) - win.on('enter-full-screen', () => menu.onToggleFullScreen(true)) - win.on('leave-full-screen', () => menu.onToggleFullScreen(false)) + win.on('enter-full-screen', function () { + menu.onToggleFullScreen(true) + send('fullscreenChanged', true) + win.setMenuBarVisibility(false) + }) + + win.on('leave-full-screen', function () { + menu.onToggleFullScreen(false) + send('fullscreenChanged', false) + win.setMenuBarVisibility(true) + }) win.on('close', function (e) { if (process.platform !== 'darwin' && !tray.hasTray()) { @@ -64,15 +80,115 @@ function create () { } else if (!app.isQuitting) { e.preventDefault() win.hide() - win.send('dispatch', 'backToList') } }) } -function getIconPath () { - return process.platform === 'win32' - ? config.APP_ICON + '.ico' - : config.APP_ICON + '.png' +function dispatch (...args) { + send('dispatch', ...args) +} + +function hide () { + if (!main.win) return + main.win.send('dispatch', 'backToList') + main.win.hide() +} + +function send (...args) { + if (!main.win) return + main.win.send(...args) +} + +/** + * Enforce window aspect ratio. Remove with 0. (OS X) + */ +function setAspectRatio (aspectRatio) { + if (!main.win) return + main.win.setAspectRatio(aspectRatio) +} + +/** + * Change the size of the window. + * TODO: Clean this up? Seems overly complicated. + */ +function setBounds (bounds, maximize) { + // Do nothing in fullscreen + if (!main.win || main.win.isFullScreen()) { + log('setBounds: not setting bounds because we\'re in full screen') + return + } + + // Maximize or minimize, if the second argument is present + var willBeMaximized + if (maximize === true) { + if (!main.win.isMaximized()) { + log('setBounds: maximizing') + main.win.maximize() + } + willBeMaximized = true + } else if (maximize === false) { + if (main.win.isMaximized()) { + log('setBounds: unmaximizing') + main.win.unmaximize() + } + willBeMaximized = false + } else { + willBeMaximized = main.win.isMaximized() + } + + // Assuming we're not maximized or maximizing, set the window size + if (!willBeMaximized) { + log('setBounds: setting bounds to ' + JSON.stringify(bounds)) + if (bounds.x === null && bounds.y === null) { + // X and Y not specified? By default, center on current screen + var scr = electron.screen.getDisplayMatching(main.win.getBounds()) + bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2) + 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) + } else { + log('setBounds: not setting bounds because of window maximization') + } +} + +/** + * Set progress bar to [0, 1]. Indeterminate when > 1. Remove with < 0. + */ +function setProgress (progress) { + if (!main.win) return + main.win.setProgressBar(progress) +} + +function setTitle (title) { + if (!main.win) return + main.win.setTitle(title) +} + +function show () { + if (!main.win) return + main.win.show() +} + +// Sets whether the window should always show on top of other windows +function toggleAlwaysOnTop (flag) { + if (!main.win) return + if (flag == null) { + flag = !main.isAlwaysOnTop() + } + log(`toggleAlwaysOnTop ${flag}`) + main.setAlwaysOnTop(flag) + menu.onToggleAlwaysOnTop(flag) +} + +function toggleDevTools () { + if (!main.win) return + log('toggleDevTools') + if (main.win.webContents.isDevToolsOpened()) { + main.win.webContents.closeDevTools() + } else { + main.win.webContents.openDevTools({ detach: true }) + } } function toggleFullScreen (flag) { @@ -82,29 +198,18 @@ function toggleFullScreen (flag) { if (flag == null) flag = !main.win.isFullScreen() - log('toggleFullScreen %s', flag) + log(`toggleFullScreen ${flag}`) if (flag) { - // Fullscreen behaves oddly unless the aspect ratio is disabled. (OS X) + // Fullscreen and aspect ratio do not play well together. (OS X) main.win.setAspectRatio(0) } main.win.setFullScreen(flag) } -function send (...args) { - if (!main.win) return - main.win.send(...args) -} - -function show () { - if (!main.win) return - main.win.show() -} - -function hide () { - if (!main.win) return - main.win.hide() -} - +function getIconPath () { + return process.platform === 'win32' + ? config.APP_ICON + '.ico' + : config.APP_ICON + '.png' } diff --git a/main/windows/webtorrent.js b/main/windows/webtorrent.js index 7d6b66f4..510fa1b5 100644 --- a/main/windows/webtorrent.js +++ b/main/windows/webtorrent.js @@ -1,14 +1,17 @@ var webtorrent = module.exports = { - create, + init, send, show, + toggleDevTools, win: null } -var config = require('../../config') var electron = require('electron') -function create () { +var config = require('../../config') +var log = require('../log') + +function init () { var win = webtorrent.win = new electron.BrowserWindow({ backgroundColor: '#1E1E1E', center: true, @@ -46,3 +49,14 @@ function send (...args) { if (!webtorrent.win) return webtorrent.win.send(...args) } + +function toggleDevTools () { + if (!webtorrent.win) return + log('toggleDevTools') + if (webtorrent.win.webContents.isDevToolsOpened()) { + webtorrent.win.webContents.closeDevTools() + webtorrent.win.hide() + } else { + webtorrent.win.webContents.openDevTools({ detach: true }) + } +} diff --git a/renderer/main.js b/renderer/main.js index 3e7c4b53..bf762730 100644 --- a/renderer/main.js +++ b/renderer/main.js @@ -224,12 +224,16 @@ function dispatch (action, ...args) { if (action === 'addTorrent') { addTorrent(args[0] /* torrent */) } - if (action === 'showOpenTorrentFile') { - ipcRenderer.send('showOpenTorrentFile') /* open torrent file */ + if (action === 'openTorrentFile') { + ipcRenderer.send('openTorrentFile') /* open torrent file */ } if (action === 'showCreateTorrent') { showCreateTorrent(args[0] /* paths */) } + if (action === 'openTorrentAddress') { + state.modal = { id: 'open-torrent-address-modal' } + update() + } if (action === 'createTorrent') { createTorrent(args[0] /* options */) } @@ -511,11 +515,6 @@ function setupIpc () { ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args)) - ipcRenderer.on('showOpenTorrentAddress', function (e) { - state.modal = { id: 'open-torrent-address-modal' } - update() - }) - ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) { state.window.isFullScreen = isFullScreen if (!isFullScreen) { diff --git a/renderer/views/header.js b/renderer/views/header.js index 67e74114..31e36bb6 100644 --- a/renderer/views/header.js +++ b/renderer/views/header.js @@ -39,7 +39,7 @@ function Header (state) { + onclick=${dispatcher('openTorrentFile')}> add `