diff --git a/README.md b/README.md index 8b1fc99d..cf5cc007 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ To build for one platform: $ npm run package -- [platform] ``` -Where `[platform]` is `--darwin`, `--linux`, or `--win32`. +Where `[platform]` is `darwin`, `linux`, or `win32`. #### Windows build notes diff --git a/bin/package.js b/bin/package.js index 18504a2b..5a9cee16 100755 --- a/bin/package.js +++ b/bin/package.js @@ -15,11 +15,11 @@ var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION function build () { var platform = process.argv[2] - if (platform === '--darwin') { + if (platform === 'darwin') { buildDarwin(printDone) - } else if (platform === '--win32') { + } else if (platform === 'win32') { buildWin32(printDone) - } else if (platform === '--linux') { + } else if (platform === 'linux') { buildLinux(printDone) } else { buildDarwin(function (err, buildPath) { @@ -137,9 +137,7 @@ var linux = { build() function buildDarwin (cb) { - var appDmg = require('appdmg') var plist = require('plist') - var sign = require('electron-osx-sign') electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { if (err) return cb(err) @@ -185,6 +183,9 @@ function buildDarwin (cb) { cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`) if (process.platform === 'darwin') { + var appDmg = require('appdmg') + var sign = require('electron-osx-sign') + /* * Sign the app with Apple Developer ID certificate. We sign the app for 2 reasons: * - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by @@ -208,13 +209,14 @@ function buildDarwin (cb) { if (err) return cb(err) // Create .zip file (used by the auto-updater) - var zipPath = path.join(buildPath[0], BUILD_NAME + '.zip') + var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.zip') cp.execSync(`pushd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'} && popd`) + console.log('Created OS X .zip file.') // Create a .dmg (OS X disk image) file, for easy user installation. var dmgOpts = { basepath: config.ROOT_PATH, - target: path.join(buildPath[0], BUILD_NAME + '.dmg'), + target: path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg'), specification: { title: config.APP_NAME, icon: config.APP_ICON + '.icns', @@ -239,6 +241,7 @@ function buildDarwin (cb) { if (info.type === 'step-begin') console.log(info.title + '...') }) dmg.on('finish', function (info) { + console.log('Created OS X disk image (.dmg) file.') cb(null, buildPath) }) }) @@ -247,7 +250,33 @@ function buildDarwin (cb) { } function buildWin32 (cb) { - electronPackager(Object.assign({}, all, win32), cb) + var installer = require('electron-winstaller') + + electronPackager(Object.assign({}, all, win32), function (err, buildPath) { + if (err) return cb(err) + + console.log('Creating Windows installer...') + installer.createWindowsInstaller({ + name: config.APP_NAME, + productName: config.APP_NAME, + title: config.APP_NAME, + exe: config.APP_NAME + '.exe', + + appDirectory: buildPath[0], + outputDirectory: path.join(config.ROOT_PATH, 'dist'), + version: pkg.version, + description: config.APP_NAME, + authors: config.APP_TEAM, + iconUrl: config.APP_ICON + '.ico', + setupIcon: config.APP_ICON + '.ico', + // certificateFile: '', // TODO + // usePackageJson: false + loadingGif: path.join(config.STATIC_PATH, 'loading.gif') + }).then(function () { + console.log('Created Windows installer.') + cb(null, buildPath) + }).catch(cb) + }) } function buildLinux (cb) { diff --git a/config.js b/config.js index 3a88c94b..b0e6fbec 100644 --- a/config.js +++ b/config.js @@ -2,13 +2,15 @@ var applicationConfigPath = require('application-config-path') var path = require('path') var APP_NAME = 'WebTorrent' +var APP_TEAM = 'The WebTorrent Project' var APP_VERSION = require('./package.json').version module.exports = { - APP_COPYRIGHT: 'Copyright © 2014-2016 The WebTorrent Project', + APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), APP_ICON: path.join(__dirname, 'static', 'WebTorrent'), APP_NAME: APP_NAME, + APP_TEAM: APP_TEAM, APP_VERSION: APP_VERSION, AUTO_UPDATE_URL: 'https://webtorrent.io/app/update?version=' + APP_VERSION, diff --git a/main/register-handlers.js b/main/handlers.js similarity index 98% rename from main/register-handlers.js rename to main/handlers.js index 95e46c42..9413f84a 100644 --- a/main/register-handlers.js +++ b/main/handlers.js @@ -1,6 +1,10 @@ +module.exports = { + init +} + var log = require('./log') -module.exports = function () { +function init () { if (process.platform === 'win32') { var path = require('path') var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico') diff --git a/main/index.js b/main/index.js index cea72c7c..335b8b68 100644 --- a/main/index.js +++ b/main/index.js @@ -4,20 +4,105 @@ var app = electron.app var autoUpdater = require('./auto-updater') var config = require('../config') +var handlers = require('./handlers') var ipc = require('./ipc') var log = require('./log') var menu = require('./menu') -var registerProtocolHandler = require('./register-handlers') var shortcuts = require('./shortcuts') +var squirrelWin32 = require('./squirrel-win32') var windows = require('./windows') -// Prevent multiple instances of the app from running at the same time. New instances -// signal this instance and exit. -var shouldQuit = app.makeSingleInstance(function (newArgv) { +var shouldQuit = false +var argv = sliceArgv(process.argv) + +if (process.platform === 'win32') { + shouldQuit = squirrelWin32.handleEvent(argv[0]) + argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1) + // app.setAppUserModelId('com.squirrel.WebTorrent.WebTorrent') +} + +if (!shouldQuit) { + // Prevent multiple instances of app from running at same time. New instances signal + // this instance and quit. + shouldQuit = app.makeSingleInstance(onAppOpen) + if (shouldQuit) { + app.quit() + } +} + +if (!shouldQuit) { + init() +} + +function init () { + app.ipcReady = false // main window has finished loading and IPC is ready + app.isQuitting = false + + // Open handlers must be added as early as possible + app.on('open-file', onOpen) + app.on('open-url', onOpen) + + ipc.init() + + app.on('will-finish-launching', function () { + autoUpdater.init() + setupCrashReporter() + }) + + app.on('ready', function () { + menu.init() + windows.createMainWindow() + shortcuts.init() + if (process.platform !== 'win32') handlers.init() + }) + + app.on('ipcReady', function () { + log('Command line args:', argv) + argv.forEach(function (torrentId) { + windows.main.send('dispatch', 'onOpen', torrentId) + }) + }) + + app.on('before-quit', function () { + app.isQuitting = true + }) + + app.on('activate', function () { + if (windows.main) { + windows.main.show() + } else { + windows.createMainWindow() + } + }) + + app.on('window-all-closed', function () { + if (process.platform !== 'darwin') { + app.quit() + } + }) +} + +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. + // Electron issue: https://github.com/atom/electron/issues/4338 + setTimeout(function () { + windows.focusMainWindow() + }, 100) + } else { + argv.push(torrentId) + } +} + +function onAppOpen (newArgv) { newArgv = sliceArgv(newArgv) if (app.ipcReady) { - log('Second app instance attempted to open but was prevented') + log('Second app instance opened, but was prevented:', newArgv) windows.focusMainWindow() newArgv.forEach(function (torrentId) { @@ -26,74 +111,6 @@ var shouldQuit = app.makeSingleInstance(function (newArgv) { } else { argv.push(...newArgv) } -}) - -if (shouldQuit) { - app.quit() -} - -var argv = sliceArgv(process.argv) - -app.on('open-file', onOpen) -app.on('open-url', onOpen) -app.on('will-finish-launching', function () { - autoUpdater.init() - setupCrashReporter() -}) - -app.ipcReady = false // main window has finished loading and IPC is ready -app.isQuitting = false - -app.on('ready', function () { - menu.init() - windows.createMainWindow() - shortcuts.init() - registerProtocolHandler() -}) - -app.on('ipcReady', function () { - log('IS_PRODUCTION:', config.IS_PRODUCTION) - if (argv.length) { - log('command line args:', process.argv) - } - argv.forEach(function (torrentId) { - windows.main.send('dispatch', 'onOpen', torrentId) - }) -}) - -app.on('before-quit', function () { - app.isQuitting = true -}) - -app.on('activate', function () { - if (windows.main) { - windows.main.show() - } else { - windows.createMainWindow(menu) - } -}) - -app.on('window-all-closed', function () { - if (process.platform !== 'darwin') { - app.quit() - } -}) - -ipc.init() - -function onOpen (e, torrentId) { - e.preventDefault() - if (app.ipcReady) { - windows.main.send('dispatch', 'onOpen', torrentId) - setTimeout(function () { - // Required for magnet links opened from Chrome otherwise the confirmation dialog - // that Chrome shows causes Chrome to steal back the focus. - // Electron issue: https://github.com/atom/electron/issues/4338 - windows.focusMainWindow() - }, 100) - } else { - argv.push(torrentId) - } } function sliceArgv (argv) { diff --git a/main/squirrel-win32.js b/main/squirrel-win32.js new file mode 100644 index 00000000..b5e9550a --- /dev/null +++ b/main/squirrel-win32.js @@ -0,0 +1,134 @@ +module.exports = { + handleEvent +} + +var cp = require('child_process') +var electron = require('electron') +var fs = require('fs') +var os = require('os') +var path = require('path') +var pathExists = require('path-exists') + +var app = electron.app + +var handlers = require('./handlers') + +var exeName = path.basename(process.execPath) +var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe') + +function handleEvent (cmd) { + if (cmd === '--squirrel-install') { + // App was installed. + + // Install protocol/file handlers, desktop/start menu shortcuts. + handlers.init() + + createShortcuts(function () { + // Ensure user sees install splash screen so they realize that Setup.exe actually + // installed an application and isn't the application itself. + setTimeout(function () { + app.quit() + }, 5000) + }) + return true + } + + if (cmd === '--squirrel-updated') { + // App was updated. (Called on new version of app) + updateShortcuts(function () { + app.quit() + }) + return true + } + + if (cmd === '--squirrel-uninstall') { + // App was just uninstalled. Undo anything we did in the --squirrel-install and + // --squirrel-updated handlers + removeShortcuts(function () { + app.quit() + }) + return true + } + + if (cmd === '--squirrel-obsolete') { + // App will be updated. (Called on outgoing version of app) + app.quit() + return true + } + + if (cmd === '--squirrel-firstrun') { + // This is called on the app's first run. Do not quit, allow startup to continue. + return false + } + + return false +} + +// Spawn a command and invoke the callback when it completes with an error and the output +// from standard out. +function spawn (command, args, cb) { + var stdout = '' + + var child + try { + child = cp.spawn(command, args) + } catch (err) { + // Spawn can throw an error + process.nextTick(function () { + cb(error, stdout) + }) + return + } + + child.stdout.on('data', function (data) { + stdout += data + }) + + var error = null + child.on('error', function (processError) { + error = processError + }) + child.on('close', function (code, signal) { + if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}') + if (error) error.stdout = stdout + cb(error, stdout) + }) +} + +// 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) +} + +// Create desktop/start menu shortcuts using the Squirrel Update.exe command line API +function createShortcuts (cb) { + spawnUpdate(['--createShortcut', exeName], cb) +} + +// Update desktop/start menu shortcuts using the Squirrel Update.exe command line API +function updateShortcuts (cb) { + var homeDir = os.homedir() + if (homeDir) { + var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk') + // Check if the desktop shortcut has been previously deleted and and keep it deleted + // if it was + pathExists(desktopShortcutPath).then(function (desktopShortcutExists) { + createShortcuts(function () { + if (desktopShortcutExists) { + cb() + } else { + // Remove the unwanted desktop shortcut that was recreated + fs.unlink(desktopShortcutPath, cb) + } + }) + }) + } else { + createShortcuts(cb) + } +} + +// Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API +function removeShortcuts (cb) { + spawnUpdate(['--removeShortcut', exeName], cb) +} diff --git a/main/windows.js b/main/windows.js index c3541ec7..e43830f3 100644 --- a/main/windows.js +++ b/main/windows.js @@ -13,7 +13,6 @@ var menu = require('./menu') function createMainWindow () { var win = windows.main = new electron.BrowserWindow({ - autoHideMenuBar: true, // Hide top menu bar unless Alt key is pressed (Windows, Linux) backgroundColor: '#282828', darkTheme: true, // Forces dark theme (GTK+3) icon: config.APP_ICON + '.png', diff --git a/package.json b/package.json index 197832ff..aee14943 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "webtorrent-app", + "name": "WebTorrent", "description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.", "version": "0.0.1", "author": { @@ -24,6 +24,7 @@ "mkdirp": "^0.5.1", "musicmetadata": "^2.0.2", "network-address": "^1.1.0", + "path-exists": "^2.1.0", "prettier-bytes": "^1.0.1", "upload-element": "^1.0.1", "virtual-dom": "^2.1.1", @@ -34,6 +35,7 @@ "electron-osx-sign": "^0.3.0", "electron-packager": "^5.0.0", "electron-prebuilt": "0.37.2", + "electron-winstaller": "^2.0.5", "gh-release": "^2.0.2", "path-exists": "^2.1.0", "plist": "^1.2.0", @@ -58,7 +60,7 @@ "scripts": { "clean": "node ./bin/clean.js", "debug": "DEBUG=* electron .", - "package": "npm prune && npm dedupe && node ./bin/package.js", + "package": "npm install && npm prune && npm dedupe && node ./bin/package.js", "size": "npm run package -- --darwin && du -ch dist/WebTorrent-darwin-x64 | grep total", "start": "electron .", "test": "standard", diff --git a/static/loading.gif b/static/loading.gif new file mode 100644 index 00000000..f938e69f Binary files /dev/null and b/static/loading.gif differ