diff --git a/bin/package.js b/bin/package.js index c72f8475..926a6703 100755 --- a/bin/package.js +++ b/bin/package.js @@ -10,18 +10,25 @@ var electronPackager = require('electron-packager') var fs = require('fs') var path = require('path') var pkg = require('../package.json') -var plist = require('plist') + +var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION function build () { var platform = process.argv[2] if (platform === '--darwin') { - buildDarwin() + buildDarwin(printDone) } else if (platform === '--win32') { - buildWin32() + buildWin32(printDone) } else if (platform === '--linux') { - buildLinux() + buildLinux(printDone) } else { - buildDarwin(() => buildWin32(() => buildLinux())) // Build all + buildDarwin(function (err, buildPath) { + printDone(err, buildPath) + buildWin32(function (err, buildPath) { + printDone(err, buildPath) + buildLinux(printDone) + }) + }) } } @@ -30,7 +37,7 @@ var all = { arch: 'x64', // The application source directory. - dir: path.join(__dirname, '..'), + dir: config.ROOT_PATH, // The release version of the application. Maps to the `ProductVersion` metadata // property on Windows, and `CFBundleShortVersionString` on OS X. @@ -58,7 +65,7 @@ var all = { name: config.APP_NAME, // The base directory where the finished package(s) are created. - out: path.join(__dirname, '..', 'dist'), + out: path.join(config.ROOT_PATH, 'dist'), // Replace an already existing output directory. overwrite: true, @@ -130,18 +137,15 @@ var linux = { build() function buildDarwin (cb) { - electronPackager(Object.assign({}, all, darwin), function (err, appPath) { - printDone(err, appPath) + 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) - var contentsPath = path.join( - __dirname, - '..', - 'dist', - `${config.APP_NAME}-darwin-x64`, - `${config.APP_NAME}.app`, - 'Contents' - ) + var appPath = path.join(buildPath[0], config.APP_NAME + '.app') + var contentsPath = path.join(appPath, 'Contents') var resourcesPath = path.join(contentsPath, 'Resources') var infoPlistPath = path.join(contentsPath, 'Info.plist') var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) @@ -178,25 +182,70 @@ function buildDarwin (cb) { fs.writeFileSync(infoPlistPath, plist.build(infoPlist)) cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`) - if (cb) cb(null) + var zipPath = path.join(buildPath[0], BUILD_NAME + '.zip') + cp.execSync(`zip -r -y ${zipPath} ${appPath}`) + + /* + * Signing OS X apps for distribution outside the App Store requires: + * + * - Xcode + * - Xcode Command Line Tools (xcode-select --install) + * - Membership in the Apple Developer Program + */ + if (process.platform === 'darwin') { + var signOpts = { + app: appPath, + platform: 'darwin', + verbose: true + } + + var dmgPath = path.join(buildPath[0], BUILD_NAME + '.dmg') + var dmgOpts = { + basepath: config.ROOT_PATH, + target: dmgPath, + specification: { + title: config.APP_NAME, + icon: config.APP_ICON + '.icns', + background: path.join(config.STATIC_PATH, 'appdmg.png'), + 'icon-size': 128, + contents: [ + { x: 122, y: 240, type: 'file', path: appPath }, + { x: 380, y: 240, type: 'link', path: '/Applications' }, + // Hide hidden icons out of view, for users who have hidden files shown. + // https://github.com/LinusU/node-appdmg/issues/45#issuecomment-153924954 + { x: 50, y: 500, type: 'position', path: '.background' }, + { x: 100, y: 500, type: 'position', path: '.DS_Store' }, + { x: 150, y: 500, type: 'position', path: '.Trashes' }, + { x: 200, y: 500, type: 'position', path: '.VolumeIcon.icns' } + ] + } + } + + sign(signOpts, function (err) { + if (err) return cb(err) + + var dmg = appDmg(dmgOpts) + dmg.on('error', cb) + dmg.on('progress', function (info) { + if (info.type === 'step-begin') console.log(info.title + '...') + }) + dmg.on('finish', function (info) { + cb(null, buildPath) + }) + }) + } }) } function buildWin32 (cb) { - electronPackager(Object.assign({}, all, win32), function (err, appPath) { - printDone(err, appPath) - if (cb) cb(err) - }) + electronPackager(Object.assign({}, all, win32), cb) } function buildLinux (cb) { - electronPackager(Object.assign({}, all, linux), function (err, appPath) { - printDone(err, appPath) - if (cb) cb(err) - }) + electronPackager(Object.assign({}, all, linux), cb) } -function printDone (err, appPath) { +function printDone (err, buildPath) { if (err) console.error(err.message || err) - else console.log('Built ' + appPath) + else console.log('Built ' + buildPath[0]) } diff --git a/config.js b/config.js index 1fb24458..51031da4 100644 --- a/config.js +++ b/config.js @@ -1,20 +1,28 @@ var applicationConfigPath = require('application-config-path') var path = require('path') +var APP_NAME = 'WebTorrent' +var APP_VERSION = require('./package.json').version + module.exports = { APP_COPYRIGHT: 'Copyright © 2014-2016 The WebTorrent Project', APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), APP_ICON: path.join(__dirname, 'static', 'WebTorrent'), - APP_NAME: 'WebTorrent', + APP_NAME: APP_NAME, + APP_VERSION: APP_VERSION, - CONFIG_PATH: applicationConfigPath('WebTorrent'), - CONFIG_POSTER_PATH: path.join(applicationConfigPath('WebTorrent'), 'Posters'), - CONFIG_TORRENT_PATH: path.join(applicationConfigPath('WebTorrent'), 'Torrents'), + AUTO_UPDATE_URL: 'https://webtorrent.io/app/update?version=' + APP_VERSION, + AUTO_UPDATE_CHECK_STARTUP_DELAY: 60 * 1000 /* 1 minute */, + + CONFIG_PATH: applicationConfigPath(APP_NAME), + CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'), + CONFIG_TORRENT_PATH: path.join(applicationConfigPath(APP_NAME), 'Torrents'), INDEX: 'file://' + path.join(__dirname, 'renderer', 'index.html'), IS_PRODUCTION: isProduction(), + ROOT_PATH: __dirname, STATIC_PATH: path.join(__dirname, 'static'), SOUND_ADD: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'), diff --git a/index.js b/index.js index 500486c1..151415b0 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,2 @@ console.time('init') require('./main') - -// report crashes -// require('crash-reporter').start({ -// productName: 'WebTorrent', -// companyName: 'WebTorrent', -// submitURL: 'https://webtorrent.io/crash-report', -// autoSubmit: true -// }) diff --git a/main/auto-updater.js b/main/auto-updater.js new file mode 100644 index 00000000..37a400b1 --- /dev/null +++ b/main/auto-updater.js @@ -0,0 +1,31 @@ +module.exports = { + init +} + +var electron = require('electron') + +var config = require('../config') +var log = require('./log') + +var autoUpdater = electron.autoUpdater + +function init () { + autoUpdater.on('error', function (err) { + log.error('App update error: ' + err.message || err) + }) + + autoUpdater.setFeedURL(config.AUTO_UPDATE_URL) + + /* + * We always check for updates on app startup. To keep app startup fast, we delay this + * first check so it happens when there is less going on. + */ + setTimeout(() => autoUpdater.checkForUpdates(), config.AUTO_UPDATE_CHECK_STARTUP_DELAY) + + autoUpdater.on('checking-for-update', () => log('Checking for app update')) + autoUpdater.on('update-available', () => log('App update available')) + autoUpdater.on('update-not-available', () => log('App update not available')) + autoUpdater.on('update-downloaded', function (e, releaseNotes, releaseName, releaseDate, updateURL) { + log('App update downloaded: ', releaseName, updateURL) + }) +} diff --git a/main/index.js b/main/index.js index 7ae290f5..34ab4729 100644 --- a/main/index.js +++ b/main/index.js @@ -2,8 +2,10 @@ var electron = require('electron') var app = electron.app +var autoUpdater = require('./auto-updater') var config = require('../config') var ipc = require('./ipc') +var log = require('./log') var menu = require('./menu') var registerProtocolHandler = require('./register-handlers') var shortcuts = require('./shortcuts') @@ -15,7 +17,7 @@ var shouldQuit = app.makeSingleInstance(function (newArgv) { newArgv = sliceArgv(newArgv) if (app.ipcReady) { - windows.main.send('log', 'Second app instance attempted to open but was prevented') + log('Second app instance attempted to open but was prevented') newArgv.forEach(function (torrentId) { windows.main.send('dispatch', 'onOpen', torrentId) @@ -38,6 +40,10 @@ 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 @@ -50,9 +56,9 @@ app.on('ready', function () { }) app.on('ipcReady', function () { - windows.main.send('log', 'IS_PRODUCTION:', config.IS_PRODUCTION) + log('IS_PRODUCTION:', config.IS_PRODUCTION) if (argv.length) { - windows.main.send('log', 'command line args:', process.argv) + log('command line args:', process.argv) } argv.forEach(function (torrentId) { windows.main.send('dispatch', 'onOpen', torrentId) @@ -91,3 +97,12 @@ function onOpen (e, torrentId) { function sliceArgv (argv) { return argv.slice(config.IS_PRODUCTION ? 1 : 2) } + +function setupCrashReporter () { + // require('crash-reporter').start({ + // productName: 'WebTorrent', + // companyName: 'WebTorrent', + // submitURL: 'https://webtorrent.io/crash-report', + // autoSubmit: true + // }) +} diff --git a/main/ipc.js b/main/ipc.js index e610cad5..ffbdad46 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -9,6 +9,7 @@ var app = electron.app var ipcMain = electron.ipcMain var powerSaveBlocker = electron.powerSaveBlocker +var log = require('./log') var menu = require('./menu') var windows = require('./windows') @@ -51,7 +52,7 @@ function init () { }) ipcMain.on('openItem', function (e, path) { - console.log('opening file or folder: ' + path) + log('opening file or folder: ' + path) electron.shell.openItem(path) }) diff --git a/main/log.js b/main/log.js new file mode 100644 index 00000000..bc064135 --- /dev/null +++ b/main/log.js @@ -0,0 +1,31 @@ +module.exports = log +module.exports.error = error + +/** + * In the main electron process, we do not use console.log() statements because they do + * not show up in a convenient location when running the packaged (i.e. production) + * version of the app. Instead use this module, which sends the logs to the main window + * where they can be viewed in Developer Tools. + */ + +var electron = require('electron') + +var windows = require('./windows') + +var app = electron.app + +function log (...args) { + if (app.ipcReady) { + windows.main.send('log', ...args) + } else { + app.on('ipcReady', () => windows.main.send('log', ...args)) + } +} + +function error (...args) { + if (app.ipcReady) { + windows.main.send('error', ...args) + } else { + app.on('ipcReady', () => windows.main.send('error', ...args)) + } +} diff --git a/main/register-handlers.js b/main/register-handlers.js index a9a21902..95e46c42 100644 --- a/main/register-handlers.js +++ b/main/register-handlers.js @@ -1,3 +1,5 @@ +var log = require('./log') + module.exports = function () { if (process.platform === 'win32') { var path = require('path') @@ -86,7 +88,7 @@ function registerProtocolHandlerWin32 (protocol, name, icon, command) { commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', callback) function callback (err) { - if (err) console.error(err.message || err) + if (err) log.error(err.message || err) } } @@ -118,6 +120,6 @@ function registerFileHandlerWin32 (ext, id, name, icon, command) { commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', callback) function callback (err) { - if (err) console.error(err.message || err) + if (err) log.error(err.message || err) } } diff --git a/package.json b/package.json index fef479c6..a3fa6bad 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "winreg": "^1.0.1" }, "devDependencies": { + "appdmg": "^0.3.6", + "electron-osx-sign": "^0.3.0", "electron-packager": "^5.0.0", "electron-prebuilt": "0.37.2", "path-exists": "^2.1.0", diff --git a/renderer/index.js b/renderer/index.js index 810ace96..b8412149 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -283,6 +283,7 @@ function setupIpc () { ipcRenderer.send('ipcReady') ipcRenderer.on('log', (e, ...args) => console.log(...args)) + ipcRenderer.on('error', (e, ...args) => console.error(...args)) ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args)) diff --git a/renderer/state.js b/renderer/state.js index 3f08ec44..373b893b 100644 --- a/renderer/state.js +++ b/renderer/state.js @@ -65,8 +65,8 @@ module.exports = { status: 'paused', infoHash: '88594aaacbde40ef3e2510c47374ec0aa396c08e', displayName: 'Big Buck Bunny', - posterURL: path.join(__dirname, '..', 'static', 'bigBuckBunny.jpg'), - torrentPath: path.join(__dirname, '..', 'static', 'bigBuckBunny.torrent'), + posterURL: path.join(config.ROOT_PATH, 'static', 'bigBuckBunny.jpg'), + torrentPath: path.join(config.ROOT_PATH, 'static', 'bigBuckBunny.torrent'), files: [ { 'name': 'bbb_sunflower_1080p_30fps_normal.mp4', @@ -80,8 +80,8 @@ module.exports = { status: 'paused', infoHash: '6a9759bffd5c0af65319979fb7832189f4f3c35d', displayName: 'Sintel', - posterURL: path.join(__dirname, '..', 'static', 'sintel.jpg'), - torrentPath: path.join(__dirname, '..', 'static', 'sintel.torrent'), + posterURL: path.join(config.ROOT_PATH, 'static', 'sintel.jpg'), + torrentPath: path.join(config.ROOT_PATH, 'static', 'sintel.torrent'), files: [ { 'name': 'sintel.mp4', @@ -95,8 +95,8 @@ module.exports = { status: 'paused', infoHash: '02767050e0be2fd4db9a2ad6c12416ac806ed6ed', displayName: 'Tears of Steel', - posterURL: path.join(__dirname, '..', 'static', 'tearsOfSteel.jpg'), - torrentPath: path.join(__dirname, '..', 'static', 'tearsOfSteel.torrent'), + posterURL: path.join(config.ROOT_PATH, 'static', 'tearsOfSteel.jpg'), + torrentPath: path.join(config.ROOT_PATH, 'static', 'tearsOfSteel.torrent'), files: [ { 'name': 'tears_of_steel_1080p.webm', diff --git a/static/appdmg.png b/static/appdmg.png new file mode 100644 index 00000000..023e7130 Binary files /dev/null and b/static/appdmg.png differ diff --git a/static/appdmg@2x.png b/static/appdmg@2x.png new file mode 100644 index 00000000..5874b7ef Binary files /dev/null and b/static/appdmg@2x.png differ