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 }) diff --git a/main/announcement.js b/main/announcement.js index 1e2801d5..275bc0de 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') @@ -12,27 +11,47 @@ 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 () { - 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 () {}) - }) + var get = require('simple-get') + get.concat(ANNOUNCEMENT_URL, onResponse) } + +function onResponse (err, res, data) { + 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: data.toString(), + detail: data.toString() + } + } + + electron.dialog.showMessageBox({ + type: 'info', + buttons: ['OK'], + title: data.title, + message: data.message, + detail: data.detail + }, noop) +} + +function noop () {} diff --git a/main/dialog.js b/main/dialog.js new file mode 100644 index 00000000..6fe06ab1 --- /dev/null +++ b/main/dialog.js @@ -0,0 +1,97 @@ +module.exports = { + openSeedFile, + openSeedDirectory, + openTorrentFile, + openTorrentAddress +} + +var electron = require('electron') + +var config = require('../config') +var log = require('./log') +var windows = require('./windows') + +/** + * Show open dialog to create a single-file torrent. + */ +function openSeedFile () { + if (!windows.main.win) return + log('openSeedFile') + var opts = { + title: 'Select a file for the torrent.', + properties: [ 'openFile' ] + } + setTitle(opts.title) + electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) { + resetTitle() + if (!Array.isArray(selectedPaths)) return + windows.main.dispatch('showCreateTorrent', selectedPaths) + }) +} + +/* + * 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, + * so this function shows a directory dialog on those platforms. + */ +function openSeedDirectory () { + if (!windows.main.win) return + log('openSeedDirectory') + 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.dispatch('showCreateTorrent', selectedPaths) + }) +} + +/* + * Show open dialog to open a .torrent file. + */ +function openTorrentFile () { + if (!windows.main.win) return + log('openTorrentFile') + 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.dispatch('addTorrent', selectedPath) + }) + }) +} + +/* + * Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.) + */ +function openTorrentAddress () { + log('openTorrentAddress') + windows.main.dispatch('openTorrentAddress') +} + +/** + * 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.dispatch('setTitle', title) + } +} + +function resetTitle () { + setTitle(config.APP_WINDOW_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/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) } diff --git a/main/index.js b/main/index.js index 92f7dacb..2d2c8b93 100644 --- a/main/index.js +++ b/main/index.js @@ -8,6 +8,8 @@ var ipcMain = electron.ipcMain 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') @@ -60,8 +62,8 @@ function init () { app.on('ready', function () { isReady = true - windows.createMainWindow() - windows.createWebTorrentHiddenWindow() + windows.main.init() + windows.webtorrent.init() menu.init() // To keep app startup fast, some code is delayed. @@ -79,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 */ + setTimeout(() => app.quit(), 2000) // quit after 2 secs, at most }) app.on('activate', function () { - if (isReady) windows.createMainWindow() + if (isReady) windows.main.show() }) } function delayedInit () { announcement.init() - tray.init() + dock.init() handlers.install() + tray.init() updater.init() } @@ -100,12 +103,12 @@ function onOpen (e, torrentId) { e.preventDefault() 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. + 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 setTimeout(function () { - windows.focusWindow(windows.main) + windows.main.show() }, 100) } else { argv.push(torrentId) @@ -114,10 +117,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.show() processArgv(newArgv) } else { @@ -130,27 +134,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.dispatch('onOpen', paths) + } } diff --git a/main/ipc.js b/main/ipc.js index 17bec585..b9c4c59a 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -5,31 +5,33 @@ module.exports = { var electron = require('electron') var app = electron.app -var ipcMain = electron.ipcMain +var dialog = require('./dialog') +var dock = require('./dock') var log = require('./log') var menu = require('./menu') -var windows = require('./windows') +var powerSaveBlocker = require('./power-save-blocker') +var shell = require('./shell') 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 () { - ipcMain.on('ipcReady', function (e) { + var ipc = electron.ipcMain + + 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) @@ -39,113 +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) { - menu.toggleFullScreen(flag) - }) - - ipcMain.on('setTitle', function (e, title) { - windows.main.setTitle(title) - }) - - ipcMain.on('openItem', function (e, path) { - 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', blockPowerSave) - - ipcMain.on('unblockPowerSave', unblockPowerSave) - - 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('focusWindow', function (e, windowName) { - windows.focusWindow(windows[windowName]) - }) + /** + * Power Save Blocker + */ - 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) - } - }) + ipc.on('blockPowerSave', () => powerSaveBlocker.start()) + ipc.on('unblockPowerSave', () => powerSaveBlocker.stop()) - ipcMain.on('checkForVLC', function (e) { + /** + * 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') { @@ -168,82 +168,6 @@ function init () { } // Emit all other events normally - oldEmit.call(ipcMain, name, e, ...args) - } -} - -function setBounds (bounds, maximize) { - // Do nothing in fullscreen - if (!windows.main || windows.main.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.isMaximized()) { - log('setBounds: maximizing') - windows.main.maximize() - } - willBeMaximized = true - } else if (maximize === false) { - if (windows.main.isMaximized()) { - log('setBounds: unmaximizing') - windows.main.unmaximize() - } - willBeMaximized = false - } else { - willBeMaximized = windows.main.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.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) - } else { - log('setBounds: not setting bounds because of window maximization') - } -} - -function setAspectRatio (aspectRatio) { - log('setAspectRatio %o', aspectRatio) - if (windows.main) { - windows.main.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) { - 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) + oldEmit.call(ipc, name, e, ...args) } } 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..640c4eba 100644 --- a/main/menu.js +++ b/main/menu.js @@ -2,15 +2,10 @@ module.exports = { init, onPlayerClose, onPlayerOpen, + onToggleAlwaysOnTop, onToggleFullScreen, - onWindowHide, - onWindowShow, - - // TODO: move these out of menu.js -- they don't belong here - showOpenSeedFiles, - showOpenTorrentAddress, - showOpenTorrentFile, - toggleFullScreen + onWindowBlur, + onWindowFocus } var electron = require('electron') @@ -18,213 +13,67 @@ var electron = require('electron') var app = electron.app var config = require('../config') -var log = require('./log') +var dialog = require('./dialog') +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) - } -} - -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) { - log('toggleFloatOnTop %s', flag) - if (windows.main) { - flag = flag != null ? flag : !windows.main.isAlwaysOnTop() - windows.main.setAlwaysOnTop(flag) - getMenuItem('Float on Top').checked = flag - } -} - -function toggleDevTools () { - log('toggleDevTools') - if (windows.main) { - windows.main.toggleDevTools() - } -} - -function showWebTorrentWindow () { - log('showWebTorrentWindow') - windows.webtorrent.show() - windows.webtorrent.webContents.openDevTools({ detach: true }) -} - -function playPause () { - if (windows.main) { - windows.main.send('dispatch', 'playPause') - } -} - -function increaseVolume () { - if (windows.main) { - windows.main.send('dispatch', 'changeVolume', 0.1) - } -} - -function decreaseVolume () { - if (windows.main) { - windows.main.send('dispatch', 'changeVolume', -0.1) - } -} - -function openSubtitles () { - if (windows.main) { - windows.main.send('dispatch', 'openSubtitles') - } -} - -function skipForward () { - if (windows.main) { - windows.main.send('dispatch', 'skip', 1) - } -} - -function skipBack () { - if (windows.main) { - windows.main.send('dispatch', 'skip', -1) - } -} - -function increasePlaybackRate () { - if (windows.main) { - windows.main.send('dispatch', 'changePlaybackRate', 1) - } -} - -function decreasePlaybackRate () { - if (windows.main) { - windows.main.send('dispatch', 'changePlaybackRate', -1) - } -} - -// Open the preferences window -function showPreferences () { - if (windows.main) { - windows.main.send('dispatch', 'preferences') - } -} - -function escapeBack () { - if (windows.main) { - 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) { - isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen() - windows.main.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 } } -// 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 () { +function getMenuTemplate () { var template = [ { label: 'File', @@ -234,17 +83,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' @@ -287,7 +136,7 @@ function getAppMenuTemplate () { { label: 'Preferences', accelerator: 'CmdOrCtrl+,', - click: () => showPreferences() + click: () => windows.main.dispatch('preferences') } ] }, @@ -300,12 +149,20 @@ function getAppMenuTemplate () { accelerator: process.platform === 'darwin' ? 'Ctrl+Command+F' : 'F11', - click: () => toggleFullScreen() + click: () => windows.toggleFullScreen() }, { label: 'Float on Top', type: 'checkbox', - click: () => toggleFloatOnTop() + click: () => windows.toggleAlwaysOnTop() + }, + { + type: 'separator' + }, + { + label: 'Go Back', + accelerator: 'Esc', + click: () => windows.main.dispatch('escapeBack') }, { type: 'separator' @@ -318,24 +175,16 @@ 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() } ] - }, - { - type: 'separator' - }, - { - label: 'Go Back', - accelerator: 'Esc', - click: escapeBack } ] }, @@ -345,7 +194,7 @@ function getAppMenuTemplate () { { label: 'Play/Pause', accelerator: 'Space', - click: playPause, + click: () => windows.main.dispatch('playPause'), enabled: false }, { @@ -354,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 }, { @@ -369,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 }, { @@ -384,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 }, { @@ -398,7 +247,7 @@ function getAppMenuTemplate () { }, { label: 'Add Subtitles File...', - click: openSubtitles, + click: () => windows.main.dispatch('openSubtitles'), enabled: false } ] @@ -409,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) } ] } @@ -441,7 +290,7 @@ function getAppMenuTemplate () { { label: 'Preferences', accelerator: 'Cmd+,', - click: () => showPreferences() + click: () => windows.main.dispatch('preferences') }, { type: 'separator' @@ -500,12 +349,13 @@ 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({ label: 'Create New Torrent from File...', - click: showOpenSeedFile + click: () => dialog.openSeedFile() }) // Help menu (Windows, Linux) @@ -515,12 +365,12 @@ function getAppMenuTemplate () { }, { label: 'About ' + config.APP_NAME, - click: windows.createAboutWindow + 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({ @@ -531,23 +381,3 @@ function getAppMenuTemplate () { return template } - -function getDockMenuTemplate () { - return [ - { - label: 'Create New Torrent...', - accelerator: 'CmdOrCtrl+N', - click: showOpenSeedFiles - }, - { - label: 'Open Torrent File...', - accelerator: 'CmdOrCtrl+O', - click: showOpenTorrentFile - }, - { - label: 'Open Torrent Address...', - accelerator: 'CmdOrCtrl+U', - click: showOpenTorrentAddress - } - ] -} diff --git a/main/power-save-blocker.js b/main/power-save-blocker.js new file mode 100644 index 00000000..c7d76e42 --- /dev/null +++ b/main/power-save-blocker.js @@ -0,0 +1,30 @@ +module.exports = { + start, + stop +} + +var electron = require('electron') +var log = require('./log') + +var blockId = 0 + +/** + * Block the system from entering low-power (sleep) mode or turning off the + * display. + */ +function start () { + 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(blockId)) { + return + } + 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 f0b40a99..73cd4ae2 100644 --- a/main/tray.js +++ b/main/tray.js @@ -1,9 +1,10 @@ module.exports = { + hasTray, init, - hasTray + onWindowBlur, + onWindowFocus } -var cp = require('child_process') var electron = require('electron') var app = electron.app @@ -11,41 +12,51 @@ 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 } +/** + * Returns true if there a tray icon is active. + */ function hasTray () { - return !!trayIcon + return !!tray } -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 +function onWindowBlur () { + if (!tray) return updateTrayMenu() - windows.main.on('show', updateTrayMenu) - windows.main.on('hide', updateTrayMenu) } +function onWindowFocus () { + if (!tray) return + updateTrayMenu() +} + +function initLinux () { + checkLinuxTraySupport(function (supportsTray) { + if (supportsTray) createTray() + }) +} + +function initWin32 () { + createTray() +} + +/** + * Check for libappindicator1 support before creating tray icon + */ 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,27 +68,44 @@ function checkLinuxTraySupport (cb) { }) } +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', () => windows.main.show()) + + // Show the tray context menu, and keep the available commands up to date + updateTrayMenu() +} + function updateTrayMenu () { - var showHideMenuItem - if (windows.main.isVisible()) { - showHideMenuItem = { label: 'Hide to tray', click: hideApp } - } else { - showHideMenuItem = { label: 'Show', click: showApp } + var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate) + tray.setContextMenu(contextMenu) +} + +function getMenuTemplate () { + return [ + getToggleItem(), + { + label: 'Quit', + click: () => app.quit() + } + ] + + function getToggleItem () { + if (windows.main.win.isVisible()) { + return { + label: 'Hide to tray', + click: () => windows.main.hide() + } + } else { + return { + label: 'Show WebTorrent', + click: () => windows.main.show() + } + } } - var contextMenu = electron.Menu.buildFromTemplate([ - showHideMenuItem, - { label: 'Quit', click: () => app.quit() } - ]) - trayIcon.setContextMenu(contextMenu) -} - -function showApp () { - windows.main.show() -} - -function hideApp () { - windows.main.hide() - windows.main.send('dispatch', 'backToList') } function getIconPath () { 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.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..7fba55d1 --- /dev/null +++ b/main/windows/about.js @@ -0,0 +1,48 @@ +var about = module.exports = { + init, + win: null +} + +var config = require('../../config') +var electron = require('electron') + +function init () { + if (about.win) { + return about.win.show() + } + + 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..f0d7a4ed --- /dev/null +++ b/main/windows/main.js @@ -0,0 +1,215 @@ +var main = module.exports = { + dispatch, + hide, + init, + send, + setAspectRatio, + setBounds, + setProgress, + setTitle, + show, + toggleAlwaysOnTop, + toggleDevTools, + 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 HEADER_HEIGHT = 37 +var TORRENT_HEIGHT = 100 + +function init () { + if (main.win) { + return main.win.show() + } + 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(main.win.isFullScreen()) + }) + + win.on('blur', function () { + menu.onWindowBlur() + tray.onWindowBlur() + }) + + win.on('focus', function () { + menu.onWindowFocus() + tray.onWindowFocus() + }) + + 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()) { + app.quit() + } else if (!app.isQuitting) { + e.preventDefault() + win.hide() + } + }) +} + +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) { + if (!main.win || !main.win.isVisible()) { + return + } + + if (flag == null) flag = !main.win.isFullScreen() + + log(`toggleFullScreen ${flag}`) + + if (flag) { + // Fullscreen and aspect ratio do not play well together. (OS X) + main.win.setAspectRatio(0) + } + + main.win.setFullScreen(flag) +} + +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 new file mode 100644 index 00000000..510fa1b5 --- /dev/null +++ b/main/windows/webtorrent.js @@ -0,0 +1,62 @@ +var webtorrent = module.exports = { + init, + send, + show, + toggleDevTools, + win: null +} + +var electron = require('electron') + +var config = require('../../config') +var log = require('../log') + +function init () { + 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) +} + +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/lib/dispatcher.js b/renderer/lib/dispatcher.js index 13778d99..eba84df7 100644 --- a/renderer/lib/dispatcher.js +++ b/renderer/lib/dispatcher.js @@ -1,36 +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 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 - _dispatch.apply(null, args) + + if (e.currentTarget.classList.contains('disabled')) { + // Ignore clicks on disabled elements + return + } + + dispatch(...args) } } return handler } - -function dispatch (...args) { - _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 98% rename from renderer/index.js rename to renderer/main.js index 0d4acf50..bf762730 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') @@ -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 */) } @@ -377,6 +381,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()) { @@ -508,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) { @@ -606,7 +608,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 +677,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++) { @@ -974,7 +978,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() } @@ -1312,7 +1319,7 @@ function showDoneNotification (torrent) { }) notif.onclick = function () { - ipcRenderer.send('focusWindow', 'main') + ipcRenderer.send('show') } sound.play('DONE') 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..31e36bb6 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` @@ -42,7 +39,7 @@ function Header (state) { + onclick=${dispatcher('openTorrentFile')}> add ` 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`