Compare commits

..

1 Commits

Author SHA1 Message Date
DC
229143ffb2 VLC support 2016-04-27 03:27:45 -07:00
35 changed files with 586 additions and 959 deletions

View File

@@ -11,12 +11,5 @@
- Dan Flettre <fletd01@yahoo.com> - Dan Flettre <fletd01@yahoo.com>
- Liam Gray <liam.r.gray@gmail.com> - Liam Gray <liam.r.gray@gmail.com>
- grunjol <grunjol@argenteam.net> - grunjol <grunjol@argenteam.net>
- Rémi Jouannet <remijouannet@users.noreply.github.com>
- Evan Miller <miller.evan815@gmail.com>
- Alex <alxmorais8@msn.com>
- Diego Rodríguez Baquero <diegorbaquero@gmail.com>
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
- gabriel <furstenheim@gmail.com>
- Rolando Guedes <rolando.guedes@3gnt.net>
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,59 +1,16 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.4.0 - 2016-05-13 ## UNRELEASED
### Added ### Added
- Better Windows support!
- Windows 32-bit build.
- Windows Portable App build.
- Windows app signing, for fewer install warnings.
- Better Linux support!
- Linux 32-bit build.
- Subtitles support!
- .srt and .vtt file support.
- Drag-and-drop files on video, or choose from file selector.
- Multiple subtitle files support.
- Stream to VLC when the audio codec is unplayable (e.g. AC3, EAC3).
- "Show in Folder" item in context menu.
- Volume slider, with mute/unmute button.
- New "Create torrent" page to modify:
- Torrent comment.
- Trackers.
- Private torrent flag.
- Use mouse wheel to increase/decrease volume.
- Bounce the Downloads stack when download completes. (OS X)
- New default torrent on first launch: The WIRED CD.
### Changed ### Changed
- Improve app startup time by 40%. - Use Squirrel.Windows 1.3.0
- UI tweaks: Reduce font size, reduce torrent list item height. - Fix installing when the app is already installed
- Add Playback menu for playback-related functionality. - Don't kill unrelated processes on uninstall
- Fix installing when the app is already installed. (Windows)
- Don't kill unrelated processes on uninstall. (Windows)
- Set "sheet offset" correctly for create torrent dialog. (OS X)
- Remove OS X-style Window menu. (Linux, Windows)
- Remove "Add Fake Airplay/Chromecast" menu items.
### Fixed ### Fixed
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is temporary. (OS X)
- When fullscreen, make controls use the full window. (OS X)
- Support creating torrents that contain .torrent files.
- Block power save while casting to a remote device.
- Do not block power save when the space key is pressed from the torrent list.
- Support playing .mpg and .ogv extensions in the app.
- Fix video centering for multi-screen setups.
- Show an error when adding a duplicate torrent.
- Show an error when adding an invalid magnet link.
- Do not stop music when tabbing to another program (OS X)
- Properly size the Windows volume mixer icon.
- Default to the user's OS-defined, localized "Downloads" folder.
- Enforce minimimum window size when resizing player, to prevent window disappearing.
- Fix rare race condition error on app quit.
- Don't use zero-byte torrent "poster" images.
## v0.3.3 - 2016-04-07 ## v0.3.3 - 2016-04-07
### Fixed ### Fixed

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env node
var fs = require('fs')
var cp = require('child_process')
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen']
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
function main () {
if (process.platform === 'win32') {
console.log('Sorry, check-deps only works on Mac and Linux')
return
}
var jsDeps = findJSDeps()
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)
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!')
}
// Finds all dependencies, required, optional, or dev, in package.json
function findPackageDeps () {
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
var requiredDeps = Object.keys(pkg.dependencies)
var devDeps = Object.keys(pkg.devDependencies)
var optionalDeps = Object.keys(pkg.optionalDependencies)
return [].concat(requiredDeps, devDeps, optionalDeps)
}
// Finds all dependencies required() in the code
function findJSDeps () {
var stdout = cp.execSync('./bin/list-deps.sh')
return stdout.toString().trim().split('\n')
}

View File

@@ -5,9 +5,9 @@
* Useful for developers. * Useful for developers.
*/ */
var fs = require('fs')
var os = require('os') var os = require('os')
var path = require('path') var path = require('path')
var pathExists = require('path-exists')
var rimraf = require('rimraf') var rimraf = require('rimraf')
var config = require('../config') var config = require('../config')
@@ -15,12 +15,7 @@ var handlers = require('../main/handlers')
rimraf.sync(config.CONFIG_PATH) rimraf.sync(config.CONFIG_PATH)
var tmpPath var tmpPath = path.join(pathExists.sync('/tmp') ? '/tmp' : os.tmpDir(), 'webtorrent')
try {
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
} catch (err) {
tmpPath = path.join(os.tmpDir(), 'webtorrent')
}
rimraf.sync(tmpPath) rimraf.sync(tmpPath)
// Uninstall .torrent file and magnet link handlers // Uninstall .torrent file and magnet link handlers

View File

@@ -1,10 +0,0 @@
#!/bin/sh
# This is a truly heinous hack, but it works pretty nicely.
# Find all modules we're requiring---even conditional requires.
grep "require('" *.js bin/ main/ renderer/ -R |
grep '.js:' |
sed "s/.*require('\([^'\/]*\).*/\1/" |
grep -v '^\.' |
sort |
uniq

View File

@@ -1,8 +0,0 @@
#!/usr/bin/env node
var config = require('../config')
var open = require('open')
var path = require('path')
var configPath = path.join(config.CONFIG_PATH, 'config.json')
open(configPath)

View File

@@ -9,7 +9,6 @@ var electronPackager = require('electron-packager')
var fs = require('fs') var fs = require('fs')
var minimist = require('minimist') var minimist = require('minimist')
var mkdirp = require('mkdirp') var mkdirp = require('mkdirp')
var os = require('os')
var path = require('path') var path = require('path')
var rimraf = require('rimraf') var rimraf = require('rimraf')
var series = require('run-series') var series = require('run-series')
@@ -19,6 +18,16 @@ var config = require('../config')
var pkg = require('../package.json') var pkg = require('../package.json')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
/*
* Path to folder with the following files:
* - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt)
*/
var CERT_PATH = process.platform === 'win32'
? 'D:'
: '/Volumes/Certs'
var DIST_PATH = path.join(config.ROOT_PATH, 'dist') var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
var argv = minimist(process.argv.slice(2), { var argv = minimist(process.argv.slice(2), {
@@ -55,6 +64,9 @@ function build () {
} }
var all = { var all = {
// Build 64 bit binaries only.
arch: 'x64',
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata // The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
// property on Windows, and `NSHumanReadableCopyright` on OS X. // property on Windows, and `NSHumanReadableCopyright` on OS X.
'app-copyright': config.APP_COPYRIGHT, 'app-copyright': config.APP_COPYRIGHT,
@@ -73,9 +85,9 @@ var all = {
'asar-unpack': 'WebTorrent*', 'asar-unpack': 'WebTorrent*',
// The build version of the application. Maps to the FileVersion metadata property on // The build version of the application. Maps to the FileVersion metadata property on
// Windows, and CFBundleVersion on OS X. Note: Windows requires the build version to // Windows, and CFBundleVersion on OS X. We're using the short git hash (e.g. 'e7d837e')
// start with a number. We're using the version of the underlying WebTorrent library. // Windows requires the build version to start with a number :/ so we stick on a prefix
'build-version': require('webtorrent/package.json').version, 'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''),
// The application source directory. // The application source directory.
dir: config.ROOT_PATH, dir: config.ROOT_PATH,
@@ -98,16 +110,12 @@ var all = {
prune: true, prune: true,
// The Electron version with which the app is built (without the leading 'v') // The Electron version with which the app is built (without the leading 'v')
version: require('electron-prebuilt/package.json').version version: pkg.dependencies['electron-prebuilt']
} }
var darwin = { var darwin = {
// Build for OS X
platform: 'darwin', platform: 'darwin',
// Build 64 bit binaries only.
arch: 'x64',
// The bundle identifier to use in the application's plist (OS X only). // The bundle identifier to use in the application's plist (OS X only).
'app-bundle-id': 'io.webtorrent.webtorrent', 'app-bundle-id': 'io.webtorrent.webtorrent',
@@ -123,12 +131,8 @@ var darwin = {
} }
var win32 = { var win32 = {
// Build for Windows.
platform: 'win32', platform: 'win32',
// Build 32 bit binaries only.
arch: 'ia32',
// Object hash of application metadata to embed into the executable (Windows only) // Object hash of application metadata to embed into the executable (Windows only)
'version-string': { 'version-string': {
@@ -157,10 +161,9 @@ var win32 = {
} }
var linux = { var linux = {
// Build for Linux.
platform: 'linux', platform: 'linux',
// Build 32 and 64 bit binaries. // Build 32/64 bit binaries.
arch: 'all' arch: 'all'
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option. // Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
@@ -174,7 +177,7 @@ function buildDarwin (cb) {
console.log('OS X: Packaging electron...') console.log('OS X: Packaging electron...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('OS X: Packaged electron. ' + buildPath) console.log('OS X: Packaged electron. ' + buildPath[0])
var appPath = path.join(buildPath[0], config.APP_NAME + '.app') var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
var contentsPath = path.join(appPath, 'Contents') var contentsPath = path.join(appPath, 'Contents')
@@ -274,7 +277,7 @@ function buildDarwin (cb) {
var inPath = path.join(buildPath[0], config.APP_NAME + '.app') var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
zip.zipSync(inPath, outPath) zip(inPath, outPath)
console.log('OS X: Created zip.') console.log('OS X: Created zip.')
} }
@@ -324,24 +327,11 @@ function buildDarwin (cb) {
function buildWin32 (cb) { function buildWin32 (cb) {
var installer = require('electron-winstaller') var installer = require('electron-winstaller')
console.log('Windows: Packaging electron...') console.log('Windows: Packaging electron...')
/*
* Path to folder with the following files:
* - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt)
*/
var CERT_PATH
try {
fs.accessSync('D:')
CERT_PATH = 'D:'
} catch (err) {
CERT_PATH = path.join(os.homedir(), 'Desktop')
}
electronPackager(Object.assign({}, all, win32), function (err, buildPath) { electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('Windows: Packaged electron. ' + buildPath) console.log('Windows: Packaged electron. ' + buildPath[0])
var signWithParams var signWithParams
if (process.platform === 'win32') { if (process.platform === 'win32') {
@@ -368,7 +358,6 @@ function buildWin32 (cb) {
function packageInstaller (cb) { function packageInstaller (cb) {
console.log('Windows: Creating installer...') console.log('Windows: Creating installer...')
installer.createWindowsInstaller({ installer.createWindowsInstaller({
appDirectory: buildPath[0], appDirectory: buildPath[0],
authors: config.APP_TEAM, authors: config.APP_TEAM,
@@ -387,15 +376,14 @@ function buildWin32 (cb) {
title: config.APP_NAME, title: config.APP_NAME,
usePackageJson: false, usePackageJson: false,
version: pkg.version version: pkg.version
}) }).then(function () {
.then(function () {
console.log('Windows: Created installer.') console.log('Windows: Created installer.')
cb(null) cb(null)
}) }).catch(cb)
.catch(cb)
} }
function packagePortable (cb) { function packagePortable (cb) {
// Create Windows portable app
console.log('Windows: Creating portable app...') console.log('Windows: Creating portable app...')
var portablePath = path.join(buildPath[0], 'Portable Settings') var portablePath = path.join(buildPath[0], 'Portable Settings')
@@ -403,7 +391,7 @@ function buildWin32 (cb) {
var inPath = path.join(DIST_PATH, path.basename(buildPath[0])) var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
zip.zipSync(inPath, outPath) zip(inPath, outPath)
console.log('Windows: Created portable app.') console.log('Windows: Created portable app.')
cb(null) cb(null)
@@ -415,7 +403,7 @@ function buildLinux (cb) {
console.log('Linux: Packaging electron...') console.log('Linux: Packaging electron...')
electronPackager(Object.assign({}, all, linux), function (err, buildPath) { electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('Linux: Packaged electron. ' + buildPath) console.log('Linux: Packaged electron. ' + buildPath[0])
var tasks = [] var tasks = []
buildPath.forEach(function (filesPath) { buildPath.forEach(function (filesPath) {
@@ -467,7 +455,7 @@ function buildLinux (cb) {
var inPath = path.join(DIST_PATH, path.basename(filesPath)) var inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
zip.zipSync(inPath, outPath) zip(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`) console.log(`Linux: Created ${destArch} zip.`)
cb(null) cb(null)

View File

@@ -11,8 +11,6 @@ while (<>) {
next if /<support\@greenkeeper.io>/; next if /<support\@greenkeeper.io>/;
next if /<ungoldman\@gmail.com>/; next if /<ungoldman\@gmail.com>/;
next if /<grunjol\@users.noreply.github.com>/; next if /<grunjol\@users.noreply.github.com>/;
next if /<dc\@DCs-MacBook.local>/;
next if /<rolandoguedes\@gmail.com>/;
$seen{$_} = push @authors, "- ", $_; $seen{$_} = push @authors, "- ", $_;
} }
END { END {

View File

@@ -1,6 +1,6 @@
var appConfig = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
var fs = require('fs')
var path = require('path') var path = require('path')
var pathExists = require('path-exists')
var APP_NAME = 'WebTorrent' var APP_NAME = 'WebTorrent'
var APP_TEAM = 'The WebTorrent Project' var APP_TEAM = 'The WebTorrent Project'
@@ -17,6 +17,7 @@ module.exports = {
APP_VERSION: APP_VERSION, APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)', APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' + AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' +
'?version=' + APP_VERSION + '&platform=' + process.platform, '?version=' + APP_VERSION + '&platform=' + process.platform,
@@ -26,14 +27,9 @@ module.exports = {
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'), CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'), CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
DELAYED_INIT: 3000 /* 3 seconds */,
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop', GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master', GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
HOME_PAGE_URL: 'https://webtorrent.io',
IS_PORTABLE: isPortable(), IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(), IS_PRODUCTION: isProduction(),
@@ -57,11 +53,7 @@ function getConfigPath () {
} }
function isPortable () { function isPortable () {
try { return process.platform === 'win32' && isProduction() && pathExists(PORTABLE_PATH)
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
} catch (err) {
return false
}
} }
function isProduction () { function isProduction () {
@@ -69,7 +61,7 @@ function isProduction () {
return false return false
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
return !/\/Electron\.app\//.test(process.execPath) return !/\/Electron\.app\/Contents\/MacOS\/Electron$/.test(process.execPath)
} }
if (process.platform === 'win32') { if (process.platform === 'win32') {
return !/\\electron\.exe$/.test(process.execPath) return !/\\electron\.exe$/.test(process.execPath)

View File

@@ -11,4 +11,5 @@ function init () {
productName: config.APP_NAME, productName: config.APP_NAME,
submitURL: config.CRASH_REPORT_URL submitURL: config.CRASH_REPORT_URL
}) })
console.log('crash reporter started')
} }

View File

@@ -1 +1,2 @@
console.time('init')
require('./main') require('./main')

50
main/auto-updater.js Normal file
View File

@@ -0,0 +1,50 @@
module.exports = {
init
}
var electron = require('electron')
var get = require('simple-get')
var config = require('../config')
var log = require('./log')
var windows = require('./windows')
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(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)
})
}
function checkForUpdates () {
// Electron's built-in auto updater only supports Mac and Windows, for now
if (process.platform !== 'linux') {
return autoUpdater.checkForUpdates()
}
// If we're on Linux, we have to do it ourselves
get.concat(config.AUTO_UPDATE_URL, function (err, res, data) {
if (err) return log('Error checking for app update: ' + err.message)
if (![200, 204].includes(res.statusCode)) return log('Error checking for app update, got HTTP ' + res.statusCode)
if (res.statusCode !== 200) return
var obj = JSON.parse(data)
windows.main.send('dispatch', 'updateAvailable', obj.version)
})
}

View File

@@ -5,8 +5,6 @@ module.exports = {
var path = require('path') var path = require('path')
var config = require('../config')
function install () { function install () {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
installDarwin() installDarwin()
@@ -44,12 +42,6 @@ function installDarwin () {
function uninstallDarwin () {} function uninstallDarwin () {}
var EXEC_COMMAND = [ process.execPath ]
if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH)
}
function installWin32 () { function installWin32 () {
var Registry = require('winreg') var Registry = require('winreg')
@@ -57,8 +49,8 @@ function installWin32 () {
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico') var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND) registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, process.execPath)
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND) registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, process.execPath)
/** /**
* To add a protocol handler, the following keys must be added to the Windows registry: * To add a protocol handler, the following keys must be added to the Windows registry:
@@ -116,7 +108,7 @@ function installWin32 () {
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command' key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
}) })
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done) commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done)
} }
function done (err) { function done (err) {
@@ -177,7 +169,7 @@ function installWin32 () {
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command' key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
}) })
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done) commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done)
} }
function done (err) { function done (err) {
@@ -189,8 +181,8 @@ function installWin32 () {
function uninstallWin32 () { function uninstallWin32 () {
var Registry = require('winreg') var Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('magnet', process.execPath)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND) unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', process.execPath)
function unregisterProtocolHandlerWin32 (protocol, command) { function unregisterProtocolHandlerWin32 (protocol, command) {
getCommand() getCommand()
@@ -201,7 +193,7 @@ function uninstallWin32 () {
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command' key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
}) })
commandKey.get('', function (err, item) { commandKey.get('', function (err, item) {
if (!err && item.value.indexOf(commandToArgs(command)) >= 0) { if (!err && item.value.indexOf(command) >= 0) {
destroyProtocol() destroyProtocol()
} }
}) })
@@ -249,10 +241,6 @@ function uninstallWin32 () {
} }
} }
function commandToArgs (command) {
return command.map((arg) => `"${arg}"`).join(' ')
}
function installLinux () { function installLinux () {
var fs = require('fs-extra') var fs = require('fs-extra')
var os = require('os') var os = require('os')
@@ -272,14 +260,14 @@ function installLinux () {
function writeDesktopFile (err, desktopFile) { function writeDesktopFile (err, desktopFile) {
if (err) return log.error(err.message) if (err) return log.error(err.message)
var appPath = config.IS_PRODUCTION var appPath = config.IS_PRODUCTION ? path.dirname(process.execPath) : config.ROOT_PATH
? path.dirname(process.execPath) var execPath = process.execPath + (config.IS_PRODUCTION ? '' : ' \.')
: config.ROOT_PATH var tryExecPath = process.execPath
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME) desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath) desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' ')) desktopFile = desktopFile.replace(/\$EXEC_PATH/g, execPath)
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath) desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, tryExecPath)
var desktopFilePath = path.join( var desktopFilePath = path.join(
os.homedir(), os.homedir(),

View File

@@ -1,10 +1,9 @@
console.time('init')
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
var ipcMain = electron.ipcMain var ipcMain = electron.ipcMain
var autoUpdater = require('./auto-updater')
var config = require('../config') var config = require('../config')
var crashReporter = require('../crash-reporter') var crashReporter = require('../crash-reporter')
var handlers = require('./handlers') var handlers = require('./handlers')
@@ -13,9 +12,8 @@ var log = require('./log')
var menu = require('./menu') var menu = require('./menu')
var shortcuts = require('./shortcuts') var shortcuts = require('./shortcuts')
var squirrelWin32 = require('./squirrel-win32') var squirrelWin32 = require('./squirrel-win32')
var tray = require('./tray')
var updater = require('./updater')
var windows = require('./windows') var windows = require('./windows')
var tray = require('./tray')
var shouldQuit = false var shouldQuit = false
var argv = sliceArgv(process.argv) var argv = sliceArgv(process.argv)
@@ -54,22 +52,21 @@ function init () {
app.on('will-finish-launching', function () { app.on('will-finish-launching', function () {
crashReporter.init() crashReporter.init()
autoUpdater.init()
}) })
app.on('ready', function () { app.on('ready', function () {
menu.init()
windows.createMainWindow() windows.createMainWindow()
windows.createWebTorrentHiddenWindow() windows.createWebTorrentHiddenWindow()
menu.init()
shortcuts.init() shortcuts.init()
tray.init()
// To keep app startup fast, some code is delayed. handlers.install()
setTimeout(delayedInit, config.DELAYED_INIT)
}) })
app.on('ipcReady', function () { app.on('ipcReady', function () {
log('Command line args:', argv) log('Command line args:', argv)
processArgv(argv) processArgv(argv)
console.timeEnd('init')
}) })
app.on('before-quit', function (e) { app.on('before-quit', function (e) {
@@ -87,12 +84,6 @@ function init () {
}) })
} }
function delayedInit () {
tray.init()
handlers.install()
updater.init()
}
function onOpen (e, torrentId) { function onOpen (e, torrentId) {
e.preventDefault() e.preventDefault()
@@ -129,11 +120,11 @@ function sliceArgv (argv) {
function processArgv (argv) { function processArgv (argv) {
argv.forEach(function (arg) { argv.forEach(function (arg) {
if (arg === '-n') { if (arg === '-n') {
menu.showOpenSeedFiles() windows.main.send('dispatch', 'showOpenSeedFiles')
} else if (arg === '-o') { } else if (arg === '-o') {
menu.showOpenTorrentFile() windows.main.send('dispatch', 'showOpenTorrentFile')
} else if (arg === '-u') { } else if (arg === '-u') {
menu.showOpenTorrentAddress() windows.main.send('showOpenTorrentAddress')
} else if (arg.startsWith('-psn')) { } else if (arg.startsWith('-psn')) {
// Ignore OS X launchd "process serial number" argument // Ignore OS X launchd "process serial number" argument
// More: https://github.com/feross/webtorrent-desktop/issues/214 // More: https://github.com/feross/webtorrent-desktop/issues/214

View File

@@ -6,29 +6,25 @@ var electron = require('electron')
var app = electron.app var app = electron.app
var ipcMain = electron.ipcMain var ipcMain = electron.ipcMain
var powerSaveBlocker = electron.powerSaveBlocker
var log = require('./log') var log = require('./log')
var menu = require('./menu') var menu = require('./menu')
var windows = require('./windows') var windows = require('./windows')
var shortcuts = require('./shortcuts') var shortcuts = require('./shortcuts')
var vlc = require('./vlc')
// has to be a number, not a boolean, and undefined throws an error // has to be a number, not a boolean, and undefined throws an error
var powerSaveBlockerId = 0 var powerSaveBlockID = 0
// 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 () { function init () {
ipcMain.on('ipcReady', function (e) { ipcMain.on('ipcReady', function (e) {
windows.main.show()
app.ipcReady = true app.ipcReady = true
app.emit('ipcReady') app.emit('ipcReady')
windows.main.show()
console.timeEnd('init')
}) })
var messageQueueMainToWebTorrent = []
ipcMain.on('ipcReadyWebTorrent', function (e) { ipcMain.on('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window', log('sending %d queued messages from the main win to the webtorrent window',
@@ -40,13 +36,14 @@ function init () {
}) })
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile) ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
ipcMain.on('showOpenSeedFiles', menu.showOpenSeedFiles)
ipcMain.on('setBounds', function (e, bounds, maximize) { ipcMain.on('setBounds', function (e, bounds, maximize) {
setBounds(bounds, maximize) setBounds(bounds, maximize)
}) })
ipcMain.on('setAspectRatio', function (e, aspectRatio) { ipcMain.on('setAspectRatio', function (e, aspectRatio, extraSize) {
setAspectRatio(aspectRatio) setAspectRatio(aspectRatio, extraSize)
}) })
ipcMain.on('setBadge', function (e, text) { ipcMain.on('setBadge', function (e, text) {
@@ -66,83 +63,26 @@ function init () {
}) })
ipcMain.on('openItem', function (e, path) { ipcMain.on('openItem', function (e, path) {
log('open item: ' + path) log('opening file or folder: ' + path)
electron.shell.openItem(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('blockPowerSave', blockPowerSave)
ipcMain.on('unblockPowerSave', unblockPowerSave) ipcMain.on('unblockPowerSave', unblockPowerSave)
ipcMain.on('onPlayerOpen', function () { ipcMain.on('onPlayerOpen', function () {
menu.onPlayerOpen() menu.onPlayerOpen()
shortcuts.onPlayerOpen() shortcuts.registerPlayerShortcuts()
}) })
ipcMain.on('onPlayerClose', function () { ipcMain.on('onPlayerClose', function () {
menu.onPlayerClose() menu.onPlayerClose()
shortcuts.onPlayerOpen() shortcuts.unregisterPlayerShortcuts()
}) })
ipcMain.on('focusWindow', function (e, windowName) { ipcMain.on('focusWindow', function (e, windowName) {
windows.focusWindow(windows[windowName]) windows.focusWindow(windows[windowName])
}) })
ipcMain.on('downloadFinished', function (e, filePath) {
if (app.dock) {
// Bounces the Downloads stack if the filePath is inside the Downloads folder.
app.dock.downloadFinished(filePath)
}
})
ipcMain.on('checkForVLC', function (e) {
vlc.checkForVLC(function (isInstalled) {
windows.main.send('checkForVLC', isInstalled)
})
})
ipcMain.on('vlcPlay', function (e, url) {
var args = ['--play-and-exit', '--quiet', url]
console.log('Running vlc ' + args.join(' '))
vlc.spawn(args, function (err, proc) {
if (err) windows.main.send('dispatch', 'vlcNotFound')
vlcProcess = proc
// If it works, close the modal after a second
var closeModalTimeout = setTimeout(() =>
windows.main.send('dispatch', 'exitModal'), 1000)
vlcProcess.on('close', function (code) {
clearTimeout(closeModalTimeout)
if (!vlcProcess) return // Killed
console.log('VLC exited with code ', code)
if (code === 0) {
windows.main.send('dispatch', 'backToList')
} else {
windows.main.send('dispatch', 'vlcNotFound')
}
vlcProcess = null
})
vlcProcess.on('error', function (e) {
console.log('VLC error', e)
})
})
})
ipcMain.on('vlcQuit', function () {
if (!vlcProcess) return
console.log('Killing VLC, pid ' + vlcProcess.pid)
vlcProcess.kill('SIGKILL') // kill -9
vlcProcess = null
})
// Capture all events // Capture all events
var oldEmit = ipcMain.emit var oldEmit = ipcMain.emit
ipcMain.emit = function (name, e, ...args) { ipcMain.emit = function (name, e, ...args) {
@@ -202,7 +142,8 @@ function setBounds (bounds, maximize) {
log('setBounds: setting bounds to ' + JSON.stringify(bounds)) log('setBounds: setting bounds to ' + JSON.stringify(bounds))
if (bounds.x === null && bounds.y === null) { if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen // X and Y not specified? By default, center on current screen
var scr = electron.screen.getDisplayMatching(windows.main.getBounds()) var screen = require('screen')
var scr = screen.getDisplayMatching(windows.main.getBounds())
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2) 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) bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
log('setBounds: centered to ' + JSON.stringify(bounds)) log('setBounds: centered to ' + JSON.stringify(bounds))
@@ -213,19 +154,17 @@ function setBounds (bounds, maximize) {
} }
} }
function setAspectRatio (aspectRatio) { function setAspectRatio (aspectRatio, extraSize) {
log('setAspectRatio %o', aspectRatio) log('setAspectRatio %o %o', aspectRatio, extraSize)
if (windows.main) { if (windows.main) {
windows.main.setAspectRatio(aspectRatio) windows.main.setAspectRatio(aspectRatio, extraSize)
} }
} }
// Display string in dock badging area (OS X) // Display string in dock badging area (OS X)
function setBadge (text) { function setBadge (text) {
log('setBadge %s', text) log('setBadge %s', text)
if (app.dock) { if (app.dock) app.dock.setBadge(String(text))
app.dock.setBadge(String(text))
}
} }
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1. // Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
@@ -237,13 +176,13 @@ function setProgress (progress) {
} }
function blockPowerSave () { function blockPowerSave () {
powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep') powerSaveBlockID = powerSaveBlocker.start('prevent-display-sleep')
log('blockPowerSave %d', powerSaveBlockerId) log('blockPowerSave %d', powerSaveBlockID)
} }
function unblockPowerSave () { function unblockPowerSave () {
if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) { if (powerSaveBlocker.isStarted(powerSaveBlockID)) {
electron.powerSaveBlocker.stop(powerSaveBlockerId) powerSaveBlocker.stop(powerSaveBlockID)
log('unblockPowerSave %d', powerSaveBlockerId) log('unblockPowerSave %d', powerSaveBlockID)
} }
} }

View File

@@ -1,14 +1,11 @@
module.exports = { module.exports = {
init, init,
onPlayerClose,
onPlayerOpen,
onToggleFullScreen, onToggleFullScreen,
onWindowHide, onWindowHide,
onWindowShow, onWindowShow,
onPlayerOpen,
// TODO: move these out of menu.js -- they don't belong here onPlayerClose,
showOpenSeedFiles, showOpenSeedFiles,
showOpenTorrentAddress,
showOpenTorrentFile, showOpenTorrentFile,
toggleFullScreen toggleFullScreen
} }
@@ -21,26 +18,20 @@ var config = require('../config')
var log = require('./log') var log = require('./log')
var windows = require('./windows') var windows = require('./windows')
var appMenu var appMenu, dockMenu
function init () { function init () {
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate()) appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
electron.Menu.setApplicationMenu(appMenu) electron.Menu.setApplicationMenu(appMenu)
if (app.dock) { dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate()) if (app.dock) app.dock.setMenu(dockMenu)
app.dock.setMenu(dockMenu)
}
} }
function toggleFullScreen (flag) { function toggleFullScreen (flag) {
log('toggleFullScreen %s', flag) log('toggleFullScreen %s', flag)
if (windows.main && windows.main.isVisible()) { if (windows.main && windows.main.isVisible()) {
flag = flag != null ? flag : !windows.main.isFullScreen() 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) windows.main.setFullScreen(flag)
} }
} }
@@ -55,25 +46,6 @@ function toggleFloatOnTop (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 () { function increaseVolume () {
if (windows.main) { if (windows.main) {
windows.main.send('dispatch', 'changeVolume', 0.1) windows.main.send('dispatch', 'changeVolume', 0.1)
@@ -86,6 +58,18 @@ function decreaseVolume () {
} }
} }
function toggleDevTools () {
log('toggleDevTools')
if (windows.main) {
windows.main.toggleDevTools()
}
}
function showWebTorrentWindow () {
windows.webtorrent.show()
windows.webtorrent.webContents.openDevTools({ detach: true })
}
function onWindowShow () { function onWindowShow () {
log('onWindowShow') log('onWindowShow')
getMenuItem('Full Screen').enabled = true getMenuItem('Full Screen').enabled = true
@@ -99,15 +83,11 @@ function onWindowHide () {
} }
function onPlayerOpen () { function onPlayerOpen () {
log('onPlayerOpen')
getMenuItem('Play/Pause').enabled = true
getMenuItem('Increase Volume').enabled = true getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true getMenuItem('Decrease Volume').enabled = true
} }
function onPlayerClose () { function onPlayerClose () {
log('onPlayerClose')
getMenuItem('Play/Pause').enabled = false
getMenuItem('Increase Volume').enabled = false getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false getMenuItem('Decrease Volume').enabled = false
} }
@@ -128,29 +108,17 @@ function getMenuItem (label) {
} }
} }
// Prompts the user for a file, then creates a torrent. Only allows a single file // Prompts the user for a file or folder, then makes a torrent out of the data
// selection.
function showOpenSeedFile () {
electron.dialog.showOpenDialog({
title: 'Select a file for the torrent file.',
properties: [ 'openFile' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
var selectedPath = selectedPaths[0]
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
})
}
// 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 () { function showOpenSeedFiles () {
// Allow only a single selection
// To create a multi-file torrent, the user must select a folder
electron.dialog.showOpenDialog({ electron.dialog.showOpenDialog({
title: 'Select a file or folder for the torrent file.', title: 'Select a file or folder for the torrent file.',
properties: [ 'openFile', 'openDirectory' ] properties: [ 'openFile', 'openDirectory' ]
}, function (selectedPaths) { }, function (filenames) {
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(filenames)) return
var selectedPath = selectedPaths[0] var fileOrFolder = filenames[0]
windows.main.send('dispatch', 'showCreateTorrent', selectedPath) windows.main.send('dispatch', 'showCreateTorrent', fileOrFolder)
}) })
} }
@@ -160,10 +128,10 @@ function showOpenTorrentFile () {
title: 'Select a .torrent file to open.', title: 'Select a .torrent file to open.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ] properties: [ 'openFile', 'multiSelections' ]
}, function (selectedPaths) { }, function (filenames) {
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(filenames)) return
selectedPaths.forEach(function (selectedPath) { filenames.forEach(function (filename) {
windows.main.send('dispatch', 'addTorrent', selectedPath) windows.main.send('dispatch', 'addTorrent', filename)
}) })
}) })
} }
@@ -174,38 +142,44 @@ function showOpenTorrentAddress () {
} }
function getAppMenuTemplate () { function getAppMenuTemplate () {
var fileMenu = [
{
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
},
{
type: 'separator'
},
{
label: process.platform === 'windows' ? 'Close' : 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
// File > Quit for Linux users with distros where the system tray is broken
if (process.platform === 'linux') {
fileMenu.push({
label: 'Quit',
click: () => app.quit()
})
}
var template = [ var template = [
{ {
label: 'File', label: 'File',
submenu: [ submenu: fileMenu
{
label: process.platform === 'darwin'
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
},
{
type: 'separator'
},
{
label: process.platform === 'windows'
? 'Close'
: 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
}, },
{ {
label: 'Edit', label: 'Edit',
@@ -251,6 +225,21 @@ function getAppMenuTemplate () {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
enabled: false
},
{
type: 'separator'
},
{ {
label: 'Developer', label: 'Developer',
submenu: [ submenu: [
@@ -273,28 +262,13 @@ function getAppMenuTemplate () {
] ]
}, },
{ {
label: 'Playback', label: 'Window',
role: 'window',
submenu: [ submenu: [
{ {
label: 'Play/Pause', label: 'Minimize',
accelerator: 'CmdOrCtrl+P', accelerator: 'CmdOrCtrl+M',
click: playPause, role: 'minimize'
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
enabled: false
} }
] ]
}, },
@@ -304,7 +278,7 @@ function getAppMenuTemplate () {
submenu: [ submenu: [
{ {
label: 'Learn more about ' + config.APP_NAME, label: 'Learn more about ' + config.APP_NAME,
click: () => electron.shell.openExternal(config.HOME_PAGE_URL) click: () => electron.shell.openExternal('https://webtorrent.io')
}, },
{ {
label: 'Contribute on GitHub', label: 'Contribute on GitHub',
@@ -315,14 +289,14 @@ function getAppMenuTemplate () {
}, },
{ {
label: 'Report an Issue...', label: 'Report an Issue...',
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES) click: () => electron.shell.openExternal(config.GITHUB_URL + '/issues')
} }
] ]
} }
] ]
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// Add WebTorrent app menu (OS X) // WebTorrent menu (OS X)
template.unshift({ template.unshift({
label: config.APP_NAME, label: config.APP_NAME,
submenu: [ submenu: [
@@ -366,35 +340,17 @@ function getAppMenuTemplate () {
] ]
}) })
// Add Window menu (OS X) // Window menu (OS X)
template.splice(5, 0, { template[4].submenu.push(
label: 'Window', {
role: 'window', type: 'separator'
submenu: [ },
{ {
label: 'Minimize', label: 'Bring All to Front',
accelerator: 'CmdOrCtrl+M', role: 'front'
role: 'minimize' }
}, )
{ } else {
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
]
})
}
// In Linux and Windows it is not possible to open both folders and files
if (process.platform === 'linux' || process.platform === 'windows') {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: showOpenSeedFile
})
// Help menu (Windows, Linux) // Help menu (Windows, Linux)
template[4].submenu.push( template[4].submenu.push(
{ {
@@ -406,15 +362,6 @@ function getAppMenuTemplate () {
} }
) )
} }
// 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({
label: 'Quit',
click: () => app.quit()
})
}
return template return template
} }

View File

@@ -1,34 +1,29 @@
module.exports = { module.exports = {
init, init,
onPlayerClose, registerPlayerShortcuts,
onPlayerOpen unregisterPlayerShortcuts
} }
var electron = require('electron') var electron = require('electron')
var localShortcut = require('electron-localshortcut')
var globalShortcut = electron.globalShortcut
var menu = require('./menu') var menu = require('./menu')
var windows = require('./windows') var windows = require('./windows')
function init () { function init () {
var localShortcut = require('electron-localshortcut') // ⌘+Shift+F is an alternative fullscreen shortcut to the ones defined in menu.js.
// Electron does not support multiple accelerators for a single menu item, so this
// Alternate shortcuts. Most shortcuts are registered in menu,js, but Electron // is registered separately here.
// does not support multiple shortcuts for a single menu item.
localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen) localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen)
localShortcut.register('Space', () => windows.main.send('dispatch', 'playPause'))
// Hidden shortcuts, i.e. not shown in the menu
localShortcut.register('Esc', () => windows.main.send('dispatch', 'escapeBack'))
} }
function onPlayerOpen () { function registerPlayerShortcuts () {
// Register special "media key" for play/pause, available on some keyboards // Special "media key" for play/pause, available on some keyboards
electron.globalShortcut.register( globalShortcut.register('MediaPlayPause', () => windows.main.send('dispatch', 'playPause'))
'MediaPlayPause',
() => windows.main.send('dispatch', 'playPause')
)
} }
function onPlayerClose () { function unregisterPlayerShortcuts () {
electron.globalShortcut.unregister('MediaPlayPause') globalShortcut.unregister('MediaPlayPause')
} }

View File

@@ -8,6 +8,8 @@ var path = require('path')
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
var Menu = electron.Menu
var Tray = electron.Tray
var windows = require('./windows') var windows = require('./windows')
@@ -33,7 +35,7 @@ function hasTray () {
} }
function createTrayIcon () { function createTrayIcon () {
trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png')) trayIcon = new Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
// On Windows, left click to open the app, right click for context menu // On Windows, left click to open the app, right click for context menu
// On Linux, any click (right or left) opens the context menu // On Linux, any click (right or left) opens the context menu
@@ -64,7 +66,7 @@ function updateTrayMenu () {
} else { } else {
showHideMenuItem = { label: 'Show', click: showApp } showHideMenuItem = { label: 'Show', click: showApp }
} }
var contextMenu = electron.Menu.buildFromTemplate([ var contextMenu = Menu.buildFromTemplate([
showHideMenuItem, showHideMenuItem,
{ label: 'Quit', click: () => app.quit() } { label: 'Quit', click: () => app.quit() }
]) ])

View File

@@ -1,71 +0,0 @@
module.exports = {
init
}
var electron = require('electron')
var get = require('simple-get')
var config = require('../config')
var log = require('./log')
var windows = require('./windows')
function init () {
if (process.platform === 'linux') {
initLinux()
} else {
initDarwinWin32()
}
}
// 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(config.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 initDarwinWin32 () {
electron.autoUpdater.on(
'error',
(err) => log.error(`Update error: ${err.message}`)
)
electron.autoUpdater.on(
'checking-for-update',
() => log('Checking for update')
)
electron.autoUpdater.on(
'update-available',
() => log('Update available')
)
electron.autoUpdater.on(
'update-not-available',
() => log('Update not available')
)
electron.autoUpdater.on(
'update-downloaded',
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
)
electron.autoUpdater.setFeedURL(config.AUTO_UPDATE_URL)
}

View File

@@ -1,22 +0,0 @@
module.exports = {
checkForVLC,
spawn
}
var cp = require('child_process')
var vlcCommand = require('vlc-command')
// Finds if VLC is installed on Mac, Windows, or Linux.
// Calls back with true or false: whether VLC was detected
function checkForVLC (cb) {
vlcCommand((err) => cb(!err))
}
// Spawns VLC with child_process.spawn() to return a ChildProcess object
// Calls back with (err, childProcess)
function spawn (args, cb) {
vlcCommand(function (err, vlcPath) {
if (err) return cb(err)
cb(null, cp.spawn(vlcPath, args))
})
}

View File

@@ -9,8 +9,6 @@ var windows = module.exports = {
var electron = require('electron') var electron = require('electron')
var app = electron.app
var config = require('../config') var config = require('../config')
var menu = require('./menu') var menu = require('./menu')
var tray = require('./tray') var tray = require('./tray')
@@ -70,7 +68,7 @@ function createWebTorrentHiddenWindow () {
// Prevent killing the WebTorrent process // Prevent killing the WebTorrent process
win.on('close', function (e) { win.on('close', function (e) {
if (!app.isQuitting) { if (!electron.app.isQuitting) {
e.preventDefault() e.preventDefault()
win.hide() win.hide()
} }
@@ -81,9 +79,6 @@ function createWebTorrentHiddenWindow () {
}) })
} }
var HEADER_HEIGHT = 37
var TORRENT_HEIGHT = 100
function createMainWindow () { function createMainWindow () {
if (windows.main) { if (windows.main) {
return focusWindow(windows.main) return focusWindow(windows.main)
@@ -94,17 +89,14 @@ function createMainWindow () {
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon. icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
minWidth: config.WINDOW_MIN_WIDTH, minWidth: config.WINDOW_MIN_WIDTH,
minHeight: config.WINDOW_MIN_HEIGHT, minHeight: config.WINDOW_MIN_HEIGHT,
show: false, // Hide window until renderer sends 'ipcReady' event show: false, // Hide window until DOM finishes loading
title: config.APP_WINDOW_TITLE, title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X) titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
useContentSize: true, // Specify web page size without OS chrome useContentSize: true, // Specify web page size without OS chrome
width: 500, width: 500,
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents height: 38 + (120 * 5) // header height + 4 torrents
}) })
win.loadURL(config.WINDOW_MAIN) win.loadURL(config.WINDOW_MAIN)
if (process.platform === 'darwin') {
win.setSheetOffset(HEADER_HEIGHT)
}
win.webContents.on('dom-ready', function () { win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen() menu.onToggleFullScreen()
@@ -118,8 +110,8 @@ function createMainWindow () {
win.on('close', function (e) { win.on('close', function (e) {
if (process.platform !== 'darwin' && !tray.hasTray()) { if (process.platform !== 'darwin' && !tray.hasTray()) {
app.quit() electron.app.quit()
} else if (!app.isQuitting) { } else if (!electron.app.isQuitting) {
e.preventDefault() e.preventDefault()
win.hide() win.hide()
win.send('dispatch', 'backToList') win.send('dispatch', 'backToList')

View File

@@ -1,7 +1,7 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.4.0", "version": "0.3.3",
"author": { "author": {
"name": "Feross Aboukhadijeh", "name": "Feross Aboukhadijeh",
"email": "feross@feross.org", "email": "feross@feross.org",
@@ -15,16 +15,16 @@
}, },
"dependencies": { "dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js", "airplay-js": "guerrerocarlos/node-airplay-js",
"application-config": "^0.2.1", "application-config": "feross/node-application-config",
"bitfield": "^1.0.2", "bitfield": "^1.0.2",
"chromecasts": "^1.8.0", "chromecasts": "^1.8.0",
"concat-stream": "^1.5.1", "concat-stream": "^1.5.1",
"create-torrent": "^3.24.5", "create-torrent": "^3.24.5",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0", "dlnacasts": "^0.0.3",
"drag-drop": "^2.11.0", "drag-drop": "^2.11.0",
"electron-localshortcut": "^0.6.0", "electron-localshortcut": "^0.6.0",
"electron-prebuilt": "1.0.2", "electron-prebuilt": "0.37.6",
"fs-extra": "^0.27.0", "fs-extra": "^0.27.0",
"hyperx": "^2.0.2", "hyperx": "^2.0.2",
"languagedetect": "^1.1.1", "languagedetect": "^1.1.1",
@@ -34,35 +34,30 @@
"prettier-bytes": "^1.0.1", "prettier-bytes": "^1.0.1",
"simple-get": "^2.0.0", "simple-get": "^2.0.0",
"srt-to-vtt": "^1.1.1", "srt-to-vtt": "^1.1.1",
"upload-element": "^1.0.1",
"virtual-dom": "^2.1.1", "virtual-dom": "^2.1.1",
"vlc-command": "^1.0.1", "wcjs-player": "^0.5.7",
"webchimera.js": "^0.2.3",
"webtorrent": "0.x", "webtorrent": "0.x",
"winreg": "^1.2.0" "winreg": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"cross-zip": "^2.0.1", "cross-zip": "^1.0.0",
"electron-osx-sign": "^0.3.0", "electron-osx-sign": "^0.3.0",
"electron-packager": "^7.0.0", "electron-packager": "^7.0.0",
"electron-winstaller": "^2.3.0", "electron-winstaller": "feross/windows-installer#build",
"gh-release": "^2.0.3", "gh-release": "^2.0.3",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"nobin-debian-installer": "^0.0.9", "nobin-debian-installer": "^0.0.9",
"open": "0.0.5",
"plist": "^1.2.0", "plist": "^1.2.0",
"rimraf": "^2.5.2",
"run-series": "^1.1.4", "run-series": "^1.1.4",
"standard": "^7.0.0" "standard": "^6.0.5"
}, },
"homepage": "https://webtorrent.io", "homepage": "https://webtorrent.io",
"keywords": [ "keywords": [
"desktop", "desktop",
"electron", "electron",
"electron-app", "electron-app",
"hybrid webtorrent client",
"mad science",
"torrent client",
"torrent",
"webtorrent" "webtorrent"
], ],
"license": "MIT", "license": "MIT",
@@ -77,10 +72,13 @@
}, },
"scripts": { "scripts": {
"clean": "node ./bin/clean.js", "clean": "node ./bin/clean.js",
"open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js", "package": "node ./bin/package.js",
"start": "electron .", "start": "electron .",
"test": "standard && ./bin/check-deps.js", "test": "standard",
"update-authors": "./bin/update-authors.sh" "update-authors": "./bin/update-authors.sh"
},
"cmake-js": {
"runtime": "electron",
"runtimeVersion": "0.37.5"
} }
} }

View File

@@ -29,10 +29,7 @@
<body> <body>
<img src="../static/WebTorrent.png"> <img src="../static/WebTorrent.png">
<h1>WebTorrent</h1> <h1>WebTorrent</h1>
<p> <p>Version <script>document.write(require('../package.json').version)</script></p>
Version <script>document.write(require('../package.json').version)</script>
(<script>document.write(require('webtorrent/package.json').version)</script>)
</p>
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p> <p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
</body> </body>
</html> </html>

View File

@@ -276,6 +276,7 @@ table {
} }
.modal label { .modal label {
font-size: 16px;
font-weight: bold; font-weight: bold;
} }
@@ -433,7 +434,7 @@ input {
.torrent, .torrent,
.torrent-placeholder { .torrent-placeholder {
height: 100px; height: 120px;
} }
.torrent:not(:last-child) { .torrent:not(:last-child) {
@@ -446,9 +447,9 @@ input {
.torrent .metadata { .torrent .metadata {
position: absolute; position: absolute;
top: 25px; top: 20px;
left: 15px; left: 20px;
right: 15px; right: 20px;
width: calc(100% - 40px); width: calc(100% - 40px);
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px; text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
} }
@@ -458,15 +459,12 @@ input {
} }
.torrent .metadata span:not(:last-child)::after { .torrent .metadata span:not(:last-child)::after {
content: ' '; content: ' ';
opacity: 0.7;
padding-left: 4px;
padding-right: 4px;
} }
.torrent .buttons { .torrent .buttons {
position: absolute; position: absolute;
top: 29px; top: 25px;
right: 10px; right: 10px;
align-items: center; align-items: center;
display: none; display: none;
@@ -549,11 +547,17 @@ input {
} }
.torrent .name { .torrent .name {
font-size: 18px; font-size: 1.5em;
font-weight: bold; font-weight: bold;
line-height: 1.5em; line-height: 1.5em;
} }
.torrent .status,
.torrent .status2 {
font-size: 1em;
line-height: 1.5em;
}
/* /*
* TORRENT LIST: DRAG-DROP TARGET * TORRENT LIST: DRAG-DROP TARGET
*/ */
@@ -600,6 +604,10 @@ body.drag .app::after {
padding: 8em 20px 20px 20px; padding: 8em 20px 20px 20px;
} }
.torrent-details .open-folder {
float: right;
}
.torrent-details table { .torrent-details table {
width: 100%; width: 100%;
white-space: nowrap; white-space: nowrap;
@@ -611,7 +619,8 @@ body.drag .app::after {
height: 28px; height: 28px;
} }
.torrent-details tr:hover { .torrent-details tr:hover,
.torrent-details .open-folder:hover {
background-color: rgba(200, 200, 200, 0.3); background-color: rgba(200, 200, 200, 0.3);
} }
@@ -665,9 +674,11 @@ body.drag .app::after {
background-position: center; background-position: center;
} }
.player video { .player .video-player {
display: block; display: block;
width: 100%; width: 100%;
height: 100%;
position: relative;
} }
/* /*
@@ -751,15 +762,9 @@ body.drag .app::after {
.player-controls .volume-icon, .player-controls .volume-icon,
.player-controls .back { .player-controls .back {
display: block; display: block;
width: 20px;
height: 20px; height: 20px;
margin: 5px; margin: 5px;
/*
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
max-width: 22px;
overflow: hidden;
} }
.player-controls .volume, .player-controls .volume,
@@ -805,7 +810,6 @@ body.drag .app::after {
border: none; border: none;
padding: 0; padding: 0;
vertical-align: sub; vertical-align: sub;
-webkit-app-region: no-drag;
} }
.player-controls .volume-slider::-webkit-slider-thumb { .player-controls .volume-slider::-webkit-slider-thumb {
@@ -816,7 +820,6 @@ body.drag .app::after {
height: 10px; height: 10px;
border: 1px solid #303233; border: 1px solid #303233;
border-radius: 50%; border-radius: 50%;
-webkit-app-region: no-drag;
} }
.player-controls .volume-slider:focus { .player-controls .volume-slider:focus {
@@ -986,7 +989,3 @@ body.drag .app::after {
.error-popover .error:last-child { .error-popover .error:last-child {
border-bottom: none; border-bottom: none;
} }
.error-text {
color: #c44;
}

View File

@@ -1,24 +1,14 @@
console.time('init') console.time('init')
var crashReporter = require('../crash-reporter')
crashReporter.init()
var electron = require('electron')
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
// Listen for messages from the main process
setupIpc()
var appConfig = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
var concat = require('concat-stream') var concat = require('concat-stream')
var dragDrop = require('drag-drop') var dragDrop = require('drag-drop')
var electron = require('electron')
var fs = require('fs-extra') var fs = require('fs-extra')
var mainLoop = require('main-loop') var mainLoop = require('main-loop')
var path = require('path') var path = require('path')
var srtToVtt = require('srt-to-vtt')
var LanguageDetect = require('languagedetect')
var createElement = require('virtual-dom/create-element') var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff') var diff = require('virtual-dom/diff')
@@ -26,6 +16,7 @@ var patch = require('virtual-dom/patch')
var App = require('./views/app') var App = require('./views/app')
var config = require('../config') var config = require('../config')
var crashReporter = require('../crash-reporter')
var errors = require('./lib/errors') var errors = require('./lib/errors')
var sound = require('./lib/sound') var sound = require('./lib/sound')
var State = require('./state') var State = require('./state')
@@ -35,7 +26,18 @@ var TorrentSummary = require('./lib/torrent-summary')
var {setDispatch} = require('./lib/dispatcher') var {setDispatch} = require('./lib/dispatcher')
setDispatch(dispatch) setDispatch(dispatch)
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json') appConfig.filePath = config.CONFIG_PATH + path.sep + 'config.json'
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
var clipboard = electron.clipboard
var dialog = electron.remote.dialog
var Menu = electron.remote.Menu
var MenuItem = electron.remote.MenuItem
var remote = electron.remote
// This dependency is the slowest-loading, so we lazy load it // This dependency is the slowest-loading, so we lazy load it
var Cast = null var Cast = null
@@ -45,6 +47,10 @@ var state = global.state = State.getInitialState()
var vdomLoop var vdomLoop
// Report crashes back to our server.
// Not global JS exceptions, not like Rollbar, handles segfaults/core dumps only
crashReporter.init()
// All state lives in state.js. `state.saved` is read from and written to a file. // All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app. // All other state is ephemeral. First we load state.saved then initialize the app.
loadState(init) loadState(init)
@@ -65,7 +71,7 @@ function init () {
resumeTorrents() resumeTorrents()
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast // Lazy-load other stuff, like the AppleTV module, later to keep startup fast
window.setTimeout(delayedInit, config.DELAYED_INIT) window.setTimeout(delayedInit, 5000)
// The UI is built with virtual-dom, a minimalist library extracted from React // The UI is built with virtual-dom, a minimalist library extracted from React
// The concepts--one way data flow, a pure function that renders state to a // The concepts--one way data flow, a pure function that renders state to a
@@ -90,10 +96,16 @@ function init () {
// ...same thing if you paste a torrent // ...same thing if you paste a torrent
document.addEventListener('paste', onPaste) document.addEventListener('paste', onPaste)
// ...keyboard shortcuts
document.addEventListener('keydown', onKeyDown)
// ...focus and blur. Needed to show correct dock icon text ("badge") in OSX // ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
window.addEventListener('focus', onFocus) window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur) window.addEventListener('blur', onBlur)
// Listen for messages from the main process
setupIpc()
// Done! Ideally we want to get here <100ms after the user clicks the app // Done! Ideally we want to get here <100ms after the user clicks the app
sound.play('STARTUP') sound.play('STARTUP')
@@ -114,19 +126,10 @@ function cleanUpConfig () {
// Migration: replace torrentPath with torrentFileName // Migration: replace torrentPath with torrentFileName
var src, dst var src, dst
if (ts.torrentPath) { if (ts.torrentPath) {
// There are a number of cases to handle here:
// * Originally we used absolute paths
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
// * Finally, now we're getting rid of torrentPath altogether
console.log('migration: replacing torrentPath %s', ts.torrentPath) console.log('migration: replacing torrentPath %s', ts.torrentPath)
if (path.isAbsolute(ts.torrentPath)) { src = path.isAbsolute(ts.torrentPath)
src = ts.torrentPath ? ts.torrentPath
} else if (ts.torrentPath.startsWith('..')) { : path.join(config.STATIC_PATH, ts.torrentPath)
src = ts.torrentPath
} else {
src = path.join(config.STATIC_PATH, ts.torrentPath)
}
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent') dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
// Synchronous FS calls aren't ideal, but probably OK in a migration // Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once // that only runs once
@@ -208,6 +211,9 @@ function dispatch (action, ...args) {
if (action === 'addTorrent') { if (action === 'addTorrent') {
addTorrent(args[0] /* torrent */) addTorrent(args[0] /* torrent */)
} }
if (action === 'showOpenSeedFiles') {
ipcRenderer.send('showOpenSeedFiles') /* open file or folder to seed */
}
if (action === 'showOpenTorrentFile') { if (action === 'showOpenTorrentFile') {
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */ ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
} }
@@ -220,6 +226,9 @@ function dispatch (action, ...args) {
if (action === 'openFile') { if (action === 'openFile') {
openFile(args[0] /* infoHash */, args[1] /* index */) openFile(args[0] /* infoHash */, args[1] /* index */)
} }
if (action === 'openFolder') {
openFolder(args[0] /* infoHash */)
}
if (action === 'toggleTorrent') { if (action === 'toggleTorrent') {
toggleTorrent(args[0] /* infoHash */) toggleTorrent(args[0] /* infoHash */)
} }
@@ -242,8 +251,6 @@ function dispatch (action, ...args) {
setDimensions(args[0] /* dimensions */) setDimensions(args[0] /* dimensions */)
} }
if (action === 'backToList') { if (action === 'backToList') {
// Exit any modals and screens with a back button
state.modal = null
while (state.location.hasBack()) state.location.back() while (state.location.hasBack()) state.location.back()
// Work around virtual-dom issue: it doesn't expose its redraw function, // Work around virtual-dom issue: it doesn't expose its redraw function,
@@ -253,15 +260,6 @@ function dispatch (action, ...args) {
var mediaTag = document.querySelector('video,audio') var mediaTag = document.querySelector('video,audio')
if (mediaTag) mediaTag.pause() if (mediaTag) mediaTag.pause()
} }
if (action === 'escapeBack') {
if (state.modal) {
dispatch('exitModal')
} else if (state.window.isFullScreen) {
dispatch('toggleFullScreen')
} else {
dispatch('back')
}
}
if (action === 'back') { if (action === 'back') {
state.location.back() state.location.back()
} }
@@ -304,37 +302,20 @@ function dispatch (action, ...args) {
state.playing.isStalled = true state.playing.isStalled = true
} }
if (action === 'mediaError') { if (action === 'mediaError') {
if (state.location.current().url === 'player') { state.location.back(function () {
state.playing.location = 'error' onError(new Error('Unsupported file format'))
ipcRenderer.send('checkForVLC') })
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
state.modal = {
id: 'unsupported-media-modal',
error: args[0],
vlcInstalled: isInstalled
}
})
}
} }
if (action === 'mediaTimeUpdate') { if (action === 'mediaTimeUpdate') {
state.playing.lastTimeUpdate = new Date().getTime() state.playing.lastTimeUpdate = new Date().getTime()
state.playing.isStalled = false state.playing.isStalled = false
} }
if (action === 'mediaMouseMoved') {
state.playing.mouseStationarySince = new Date().getTime()
}
if (action === 'vlcPlay') {
ipcRenderer.send('vlcPlay', state.server.localURL)
state.playing.location = 'vlc'
}
if (action === 'vlcNotFound') {
if (state.modal && state.modal.id === 'unsupported-media-modal') {
state.modal.vlcNotFound = true
}
}
if (action === 'toggleFullScreen') { if (action === 'toggleFullScreen') {
ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */) ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
} }
if (action === 'mediaMouseMoved') {
state.playing.mouseStationarySince = new Date().getTime()
}
if (action === 'exitModal') { if (action === 'exitModal') {
state.modal = null state.modal = null
} }
@@ -384,7 +365,6 @@ function pause () {
} }
function playPause () { function playPause () {
if (state.location.current().url !== 'player') return
if (state.playing.isPaused) { if (state.playing.isPaused) {
play() play()
} else { } else {
@@ -416,7 +396,7 @@ function setVolume (volume) {
} }
function openSubtitles () { function openSubtitles () {
electron.remote.dialog.showOpenDialog({ dialog.showOpenDialog({
title: 'Select a subtitles file.', title: 'Select a subtitles file.',
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ], filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
properties: [ 'openFile' ] properties: [ 'openFile' ]
@@ -450,10 +430,6 @@ function setupIpc () {
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) { ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
state.window.isFullScreen = isFullScreen state.window.isFullScreen = isFullScreen
if (!isFullScreen) {
// Aspect ratio gets reset in fullscreen mode, so restore it (OS X)
ipcRenderer.send('setAspectRatio', state.playing.aspectRatio)
}
update() update()
}) })
@@ -539,23 +515,15 @@ function saveState () {
function onOpen (files) { function onOpen (files) {
if (!Array.isArray(files)) files = [ files ] if (!Array.isArray(files)) files = [ files ]
// In the player, the only drag-drop function is adding subtitles // .torrent file = start downloading the torrent
var isInPlayer = state.location.current().url === 'player' files.filter(isTorrent).forEach(addTorrent)
if (isInPlayer) {
return files.filter(isSubtitle).forEach(addSubtitle)
}
// Otherwise, you can only drag-drop onto the home screen // subtitle file
var isHome = state.location.current().url === 'home' && !state.modal files.filter(isSubtitle).forEach(addSubtitle)
if (isHome) {
if (files.every(isTorrent)) { // everything else = seed these files
// All .torrent files? Start downloading var rest = files.filter(not(isTorrent)).filter(not(isSubtitle))
files.forEach(addTorrent) if (rest.length > 0) showCreateTorrent(rest)
} else {
// Show the Create Torrent screen. Let's seed those files.
showCreateTorrent(files)
}
}
} }
function isTorrent (file) { function isTorrent (file) {
@@ -571,6 +539,12 @@ function isSubtitle (file) {
return ext === '.srt' || ext === '.vtt' return ext === '.srt' || ext === '.vtt'
} }
function not (test) {
return function (...args) {
return !test(...args)
}
}
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents // Gets a torrent summary {name, infoHash, status} from state.saved.torrents
// Returns undefined if we don't know that infoHash // Returns undefined if we don't know that infoHash
function getTorrentSummary (torrentKey) { function getTorrentSummary (torrentKey) {
@@ -592,9 +566,6 @@ function addTorrent (torrentId) {
} }
function addSubtitle (file) { function addSubtitle (file) {
var srtToVtt = require('srt-to-vtt')
var LanguageDetect = require('languagedetect')
if (state.playing.type !== 'video') return if (state.playing.type !== 'video') return
fs.createReadStream(file.path || file).pipe(srtToVtt()).pipe(concat(function (buf) { fs.createReadStream(file.path || file).pipe(srtToVtt()).pipe(concat(function (buf) {
// Set the cue text position so it appears above the player controls. // Set the cue text position so it appears above the player controls.
@@ -612,10 +583,9 @@ function addSubtitle (file) {
state.playing.subtitles.tracks.forEach(function (trackItem) { state.playing.subtitles.tracks.forEach(function (trackItem) {
trackItem.selected = false trackItem.selected = false
if (trackItem.label === track.label) { if (trackItem.label === track.label) {
var labelParts = /([^\d]+)(\d+)$/.exec(track.label) track.label = Number.isNaN(track.label.slice(-1))
track.label = labelParts ? track.label + ' 2'
? labelParts[1] + (parseInt(labelParts[2]) + 1) : track.label.slice(0, -1) + (parseInt(track.label.slice(-1)) + 1)
: track.label + ' 2'
} }
}) })
state.playing.subtitles.change = track.label state.playing.subtitles.change = track.label
@@ -654,7 +624,6 @@ function startTorrentingSummary (torrentSummary) {
torrentID = s.magnetURI || s.infoHash torrentID = s.magnetURI || s.infoHash
} }
console.log('start torrenting %s %s', s.torrentKey, torrentID)
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes) ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes)
} }
@@ -741,16 +710,16 @@ function torrentWarning (torrentKey, message) {
} }
function torrentError (torrentKey, message) { function torrentError (torrentKey, message) {
// TODO: WebTorrent needs semantic errors
if (message.startsWith('Cannot add duplicate torrent')) {
// Remove infohash from the message
message = 'Cannot add duplicate torrent'
}
onError(message)
var torrentSummary = getTorrentSummary(torrentKey) var torrentSummary = getTorrentSummary(torrentKey)
if (torrentSummary) {
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message) // TODO: WebTorrent should have semantic errors
if (message.startsWith('There is already a swarm')) {
onError(new Error('Can\'t add duplicate torrent'))
} else if (!torrentSummary) {
onError(message)
} else {
console.log('error, stopping torrent %s (%s):\n\t%o',
torrentSummary.name, torrentSummary.infoHash, message)
torrentSummary.status = 'paused' torrentSummary.status = 'paused'
update() update()
} }
@@ -785,7 +754,6 @@ function torrentDone (torrentKey, torrentInfo) {
state.dock.badge += 1 state.dock.badge += 1
} }
showDoneNotification(torrentSummary) showDoneNotification(torrentSummary)
ipcRenderer.send('downloadFinished', getTorrentPath(torrentSummary))
} }
update() update()
@@ -940,9 +908,6 @@ function closePlayer (cb) {
if (isCasting()) { if (isCasting()) {
Cast.close() Cast.close()
} }
if (state.playing.location === 'vlc') {
ipcRenderer.send('vlcQuit')
}
state.window.title = config.APP_WINDOW_TITLE state.window.title = config.APP_WINDOW_TITLE
state.playing = State.getDefaultPlayState() state.playing = State.getDefaultPlayState()
state.server = null state.server = null
@@ -968,6 +933,17 @@ function openFile (infoHash, index) {
ipcRenderer.send('openItem', filePath) ipcRenderer.send('openItem', filePath)
} }
function openFolder (infoHash) {
var torrentSummary = getTorrentSummary(infoHash)
var firstFilePath = path.join(
torrentSummary.path,
torrentSummary.files[0].path)
var folderPath = path.dirname(firstFilePath)
ipcRenderer.send('openItem', folderPath)
}
// TODO: use torrentKey, not infoHash // TODO: use torrentKey, not infoHash
function toggleTorrent (infoHash) { function toggleTorrent (infoHash) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
@@ -1001,46 +977,23 @@ function toggleSelectTorrent (infoHash) {
function openTorrentContextMenu (infoHash) { function openTorrentContextMenu (infoHash) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
var menu = new electron.remote.Menu() var menu = new Menu()
menu.append(new MenuItem({
if (torrentSummary.files) {
menu.append(new electron.remote.MenuItem({
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
click: () => showItemInFolder(torrentSummary)
}))
menu.append(new electron.remote.MenuItem({
type: 'separator'
}))
}
menu.append(new electron.remote.MenuItem({
label: 'Copy Magnet Link to Clipboard',
click: () => electron.clipboard.writeText(torrentSummary.magnetURI)
}))
menu.append(new electron.remote.MenuItem({
label: 'Copy Instant.io Link to Clipboard',
click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
}))
menu.append(new electron.remote.MenuItem({
label: 'Save Torrent File As...', label: 'Save Torrent File As...',
click: () => saveTorrentFileAs(torrentSummary) click: () => saveTorrentFileAs(torrentSummary)
})) }))
menu.popup(electron.remote.getCurrentWindow()) menu.append(new MenuItem({
} label: 'Copy Instant.io Link to Clipboard',
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
}))
function getTorrentPath (torrentSummary) { menu.append(new MenuItem({
var itemPath = path.join(torrentSummary.path, torrentSummary.files[0].path) label: 'Copy Magnet Link to Clipboard',
if (torrentSummary.files.length > 1) { click: () => clipboard.writeText(torrentSummary.magnetURI)
itemPath = path.dirname(itemPath) }))
}
return itemPath
}
function showItemInFolder (torrentSummary) { menu.popup(remote.getCurrentWindow())
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
} }
function saveTorrentFileAs (torrentSummary) { function saveTorrentFileAs (torrentSummary) {
@@ -1053,7 +1006,7 @@ function saveTorrentFileAs (torrentSummary) {
{ name: 'All Files', extensions: ['*'] } { name: 'All Files', extensions: ['*'] }
] ]
} }
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) { dialog.showSaveDialog(remote.getCurrentWindow(), opts, function (savePath) {
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary) var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) { fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return onError(err) if (err) return onError(err)
@@ -1067,7 +1020,7 @@ function saveTorrentFileAs (torrentSummary) {
// Set window dimensions to match video dimensions or fill the screen // Set window dimensions to match video dimensions or fill the screen
function setDimensions (dimensions) { function setDimensions (dimensions) {
// Don't modify the window size if it's already maximized // Don't modify the window size if it's already maximized
if (electron.remote.getCurrentWindow().isMaximized()) { if (remote.getCurrentWindow().isMaximized()) {
state.window.bounds = null state.window.bounds = null
return return
} }
@@ -1079,7 +1032,7 @@ function setDimensions (dimensions) {
width: window.outerWidth, width: window.outerWidth,
height: window.outerHeight height: window.outerHeight
} }
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized state.window.wasMaximized = remote.getCurrentWindow().isMaximized
// Limit window size to screen size // Limit window size to screen size
var screenWidth = window.screen.width var screenWidth = window.screen.width
@@ -1100,7 +1053,6 @@ function setDimensions (dimensions) {
ipcRenderer.send('setAspectRatio', aspectRatio) ipcRenderer.send('setAspectRatio', aspectRatio)
ipcRenderer.send('setBounds', {x: null, y: null, width, height}) ipcRenderer.send('setBounds', {x: null, y: null, width, height})
state.playing.aspectRatio = aspectRatio
} }
function restoreBounds () { function restoreBounds () {
@@ -1160,7 +1112,7 @@ function onWarning (err) {
function onPaste (e) { function onPaste (e) {
if (e.target.tagName.toLowerCase() === 'input') return if (e.target.tagName.toLowerCase() === 'input') return
var torrentIds = electron.clipboard.readText().split('\n') var torrentIds = clipboard.readText().split('\n')
torrentIds.forEach(function (torrentId) { torrentIds.forEach(function (torrentId) {
torrentId = torrentId.trim() torrentId = torrentId.trim()
if (torrentId.length === 0) return if (torrentId.length === 0) return
@@ -1168,6 +1120,20 @@ function onPaste (e) {
}) })
} }
function onKeyDown (e) {
if (e.which === 27) { /* ESC means either exit fullscreen or go back */
if (state.modal) {
dispatch('exitModal')
} else if (state.window.isFullScreen) {
dispatch('toggleFullScreen')
} else {
dispatch('back')
}
} else if (e.which === 32) { /* spacebar pauses or plays the video */
dispatch('playPause')
}
}
function onFocus (e) { function onFocus (e) {
state.window.isFocused = true state.window.isFocused = true
state.dock.badge = 0 state.dock.badge = 0

View File

@@ -21,10 +21,12 @@ function dispatcher (...args) {
var handler = _dispatchers[json] var handler = _dispatchers[json]
if (!handler) { if (!handler) {
handler = _dispatchers[json] = (e) => { handler = _dispatchers[json] = (e) => {
// Don't click on whatever is below the button if (e && e.stopPropagation && e.currentTarget) {
e.stopPropagation() // Don't click on whatever is below the button
// Don't regisiter clicks on disabled buttons e.stopPropagation()
if (e.currentTarget.classList.contains('disabled')) return // Don't register clicks on disabled buttons
if (e.currentTarget.classList.contains('disabled')) return
}
_dispatch.apply(null, args) _dispatch.apply(null, args)
} }
} }

View File

@@ -6,43 +6,41 @@ module.exports = {
var config = require('../../config') var config = require('../../config')
var path = require('path') var path = require('path')
var VOLUME = 0.15
/* Cache of Audio elements, for instant playback */ /* Cache of Audio elements, for instant playback */
var cache = {} var cache = {}
var sounds = { var sounds = {
ADD: { ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
volume: VOLUME volume: 0.2
}, },
DELETE: { DELETE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
volume: VOLUME volume: 0.1
}, },
DISABLE: { DISABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
volume: VOLUME volume: 0.2
}, },
DONE: { DONE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
volume: VOLUME volume: 0.2
}, },
ENABLE: { ENABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
volume: VOLUME volume: 0.2
}, },
ERROR: { ERROR: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
volume: VOLUME volume: 0.2
}, },
PLAY: { PLAY: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
volume: VOLUME volume: 0.2
}, },
STARTUP: { STARTUP: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
volume: VOLUME * 2 volume: 0.4
} }
} }

View File

@@ -16,27 +16,12 @@ function isPlayable (file) {
function isVideo (file) { function isVideo (file) {
var ext = path.extname(file.name).toLowerCase() var ext = path.extname(file.name).toLowerCase()
return [ return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
'.avi',
'.m4v',
'.mkv',
'.mov',
'.mp4',
'.mpg',
'.ogv',
'.webm'
].includes(ext)
} }
function isAudio (file) { function isAudio (file) {
var ext = path.extname(file.name).toLowerCase() var ext = path.extname(file.name).toLowerCase()
return [ return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
'.aac',
'.ac3',
'.mp3',
'.ogg',
'.wav'
].includes(ext)
} }
function isPlayableTorrent (torrentSummary) { function isPlayableTorrent (torrentSummary) {

View File

@@ -77,8 +77,7 @@ function getDefaultPlayState () {
subtitles: { subtitles: {
tracks: [], /* subtitles file (Buffer) */ tracks: [], /* subtitles file (Buffer) */
enabled: false enabled: false
}, }
aspectRatio: 0 /* aspect ratio of the video */
} }
} }

View File

@@ -12,8 +12,7 @@ var Views = {
} }
var Modals = { var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal'), 'open-torrent-address-modal': require('./open-torrent-address-modal'),
'update-available-modal': require('./update-available-modal'), 'update-available-modal': require('./update-available-modal')
'unsupported-media-modal': require('./unsupported-media-modal')
} }
function App (state) { function App (state) {

View File

@@ -33,6 +33,7 @@ function CreateTorrentPage (state) {
// Sanity check: show the number of files and total size // Sanity check: show the number of files and total size
var numFiles = files.length var numFiles = files.length
console.log('FILES', files)
var totalBytes = files var totalBytes = files
.map((f) => f.size) .map((f) => f.size)
.reduce((a, b) => a + b, 0) .reduce((a, b) => a + b, 0)
@@ -40,16 +41,8 @@ function CreateTorrentPage (state) {
// Then, use the name of the base folder (or sole file, for a single file torrent) // Then, use the name of the base folder (or sole file, for a single file torrent)
// as the default name. Show all files relative to the base folder. // as the default name. Show all files relative to the base folder.
var defaultName, basePath var defaultName = path.basename(pathPrefix)
if (files.length === 1) { var basePath = path.dirname(pathPrefix)
// Single file torrent: /a/b/foo.jpg -> torrent name "foo.jpg", path "/a/b"
defaultName = files[0].name
basePath = pathPrefix
} else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a"
defaultName = files[0].name
basePath = path.basename(pathPrefix)
}
var maxFileElems = 100 var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file) { var fileElems = files.slice(0, maxFileElems).map(function (file) {
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path) var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)

View File

@@ -4,6 +4,7 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx') var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var WebChimeraPlayer = require('wcjs-player')
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var Bitfield = require('bitfield') var Bitfield = require('bitfield')
@@ -18,7 +19,6 @@ function Player (state) {
return hx` return hx`
<div <div
class='player' class='player'
onwheel=${handleVolumeWheel}
onmousemove=${dispatcher('mediaMouseMoved')}> onmousemove=${dispatcher('mediaMouseMoved')}>
${showVideo ? renderMedia(state) : renderCastScreen(state)} ${showVideo ? renderMedia(state) : renderCastScreen(state)}
${renderPlayerControls(state)} ${renderPlayerControls(state)}
@@ -26,14 +26,16 @@ function Player (state) {
` `
} }
// Handles volume change by wheel
function handleVolumeWheel (e) {
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
}
function renderMedia (state) { function renderMedia (state) {
if (!state.server) return if (!state.server) return
if (false) return renderMediaTag(state)
else return renderMediaVLC(state)
}
// Renders using a <video> or <audio> tag
// Handles only a subset of codecs, but it's cleaner and more efficient
// See renderMediaVLC()
function renderMediaTag (state) {
// Unfortunately, play/pause can't be done just by modifying HTML. // Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary // Instead, grab the DOM node and play/pause it if necessary
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */ var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
@@ -71,7 +73,6 @@ function renderMedia (state) {
// Add subtitles to the <video> tag // Add subtitles to the <video> tag
var trackTags = [] var trackTags = []
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) { if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) { for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i] var track = state.playing.subtitles.tracks[i]
@@ -95,8 +96,7 @@ function renderMedia (state) {
onstalling=${dispatcher('mediaStalled')} onstalling=${dispatcher('mediaStalled')}
onerror=${dispatcher('mediaError')} onerror=${dispatcher('mediaError')}
ontimeupdate=${dispatcher('mediaTimeUpdate')} ontimeupdate=${dispatcher('mediaTimeUpdate')}
onencrypted=${dispatcher('mediaEncrypted')} autoplay>
oncanplay=${onCanPlay}>
${trackTags} ${trackTags}
</div> </div>
` `
@@ -127,15 +127,101 @@ function renderMedia (state) {
function onEnded (e) { function onEnded (e) {
state.playing.isPaused = true state.playing.isPaused = true
} }
}
function onCanPlay (e) { // Renders using WebChimera.js to render using VLC
var video = e.target // That lets us play media that the <video> tag can't play
if (video.webkitVideoDecodedByteCount > 0 && function renderMediaVLC (state) {
video.webkitAudioDecodedByteCount === 0) { // Unfortunately, WebChimera can't be done just by modifying HTML.
dispatch('mediaError', 'Audio codec unsupported') // Instead, grab the DOM node
if (document.querySelector('#media-player')) {
if (!state.playing.chimera) {
state.playing.chimera = new WebChimeraPlayer('#media-player')
.addPlayer({
autoplay: true,
vlcArgs: ['-vvv'],
wcjsRendererOptions: {'fallbackRenderer': true}
})
.onPlaying(dispatcher('mediaPlaying'))
.onPaused(dispatcher('mediaPaused'))
.onBuffering(dispatcher('mediaStalled'))
.onTime(dispatcher('mediaTimeUpdate'))
.onEnded(onEnded)
.onFrameSetup(onLoadedMetadata)
.addPlaylist(state.server.localURL)
state.playing.chimera.ui(false)
} else { } else {
video.play() var player = state.playing.chimera
if (state.playing.isPaused && player.playing()) {
player.pause()
} else if (!state.playing.isPaused && !player.playing()) {
player.play()
}
// When the user clicks or drags on the progress bar, jump to that position
if (state.playing.jumpToTime) {
player.time(state.playing.jumpToTime * 1000) // WebChimera expects milliseconds
state.playing.jumpToTime = null
}
// Set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
player.volume(Math.round(state.playing.setVolume * 100)) // WebChimera expects integer percent
state.playing.setVolume = null
}
state.playing.currentTime = player.time() / 1000
state.playing.duration = player.length() / 1000
state.playing.volume = player.volume() / 100
} }
} else {
state.playing.chimera = null
}
// Add subtitles to the <video> tag
var trackTags = []
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i]
trackTags.push(hx`
<track
default=${track.selected ? 'default' : ''}
label=${track.language}
type='subtitles'
src=${track.buffer}>
`)
}
}
// Create the <audio> or <video> tag
var mediaType = state.playing.type /* 'video' or 'audio' */
var mediaTag = hx`
<div id='media-player' class='${mediaType}-player'>
${trackTags}
</div>
`
// Show the media.
return hx`
<div
class='letterbox'
onmousemove=${dispatcher('mediaMouseMoved')}>
${mediaTag}
${renderOverlay(state)}
</div>
`
// As soon as the video loads enough to know the video dimensions, resize the window
function onLoadedMetadata (e) {
if (mediaType !== 'video') return
var dimensions = {
width: player.width(),
height: player.height()
}
dispatch('setDimensions', dimensions)
}
// When the video completes, pause the video instead of looping
function onEnded (e) {
state.playing.isPaused = true
} }
} }
@@ -224,33 +310,20 @@ function renderLoadingSpinner (state) {
} }
function renderCastScreen (state) { function renderCastScreen (state) {
var castIcon, castType, isCast var castIcon, castType
if (state.playing.location.startsWith('chromecast')) { if (state.playing.location.startsWith('chromecast')) {
castIcon = 'cast_connected' castIcon = 'cast_connected'
castType = 'Chromecast' castType = 'Chromecast'
isCast = true
} else if (state.playing.location.startsWith('airplay')) { } else if (state.playing.location.startsWith('airplay')) {
castIcon = 'airplay' castIcon = 'airplay'
castType = 'AirPlay' castType = 'AirPlay'
isCast = true
} else if (state.playing.location.startsWith('dlna')) { } else if (state.playing.location.startsWith('dlna')) {
castIcon = 'tv' castIcon = 'tv'
castType = 'DLNA' castType = 'DLNA'
isCast = true
} else if (state.playing.location === 'vlc') {
castIcon = 'tv'
castType = 'VLC'
isCast = false
} else if (state.playing.location === 'error') {
castIcon = 'error_outline'
castType = 'Error'
isCast = false
} }
var isStarting = state.playing.location.endsWith('-pending') var isStarting = state.playing.location.endsWith('-pending')
var castStatus var castStatus = isStarting ? 'Connecting...' : 'Connected'
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
else castStatus = ''
// Show a nice title image, if possible // Show a nice title image, if possible
var style = { var style = {
@@ -270,26 +343,15 @@ function renderCastScreen (state) {
function renderSubtitlesOptions (state) { function renderSubtitlesOptions (state) {
var subtitles = state.playing.subtitles var subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.show) return if (subtitles.tracks.length && subtitles.show) {
return hx`<ul.subtitles-list>
var items = subtitles.tracks.map(function (track) { ${subtitles.tracks.map(function (w, i) {
return hx` return hx`<li onclick=${dispatcher('selectSubtitle', w.label)}><i.icon>${w.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>${w.label}</li>`
<li onclick=${dispatcher('selectSubtitle', track.label)}> })}
<i.icon>${track.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i> <li onclick=${dispatcher('selectSubtitle', '')}><i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>None</li>
${track.label} </ul>
</li>
` `
}) }
return hx`
<ul.subtitles-list>
${items}
<li onclick=${dispatcher('selectSubtitle', '')}>
<i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
None
</li>
</ul>
`
} }
function renderPlayerControls (state) { function renderPlayerControls (state) {
@@ -414,7 +476,8 @@ function renderPlayerControls (state) {
} }
elements.push(hx` elements.push(hx`
<div.volume> <div.volume
onwheel=${handleVolumeWheel}>
<i.icon.volume-icon onmousedown=${handleVolumeMute}> <i.icon.volume-icon onmousedown=${handleVolumeMute}>
${volumeIcon} ${volumeIcon}
</i> </i>
@@ -423,6 +486,7 @@ function renderPlayerControls (state) {
onmousedown=${handleVolumeScrub} onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub} onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub} onmousemove=${handleVolumeScrub}
onwheel=${handleVolumeWheel}
style=${volumeStyle} style=${volumeStyle}
/> />
</div> </div>
@@ -451,6 +515,11 @@ function renderPlayerControls (state) {
dispatch('playbackJump', position) dispatch('playbackJump', position)
} }
// Handles volume change by wheel
function handleVolumeWheel (e) {
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
}
// Handles volume muting and Unmuting // Handles volume muting and Unmuting
function handleVolumeMute (e) { function handleVolumeMute (e) {
if (state.playing.volume === 0.0) { if (state.playing.volume === 0.0) {

View File

@@ -67,48 +67,38 @@ function TorrentList (state) {
// If it's downloading/seeding then show progress info // If it's downloading/seeding then show progress info
var prog = torrentSummary.progress var prog = torrentSummary.progress
if (torrentSummary.status !== 'paused' && prog) { if (torrentSummary.status !== 'paused' && prog) {
var progress = Math.floor(100 * prog.progress)
var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0)
if (downloaded !== total) downloaded += ` / ${total}`
elements.push(hx` elements.push(hx`
<div class='ellipsis'> <div class='status ellipsis'>
${renderPercentProgress()} ${getFilesLength()}
${renderTotalProgress()} <span>${getPeers()}</span>
${renderPeers()} <span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
${renderDownloadSpeed()} <span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
${renderUploadSpeed()} </div>
`)
elements.push(hx`
<div class='status2 ellipsis'>
<span class='progress'>${progress}%</span>
<span>${downloaded}</span>
</div> </div>
`) `)
} }
return hx`<div class='metadata'>${elements}</div>` return hx`<div class='metadata'>${elements}</div>`
function renderPercentProgress () { function getPeers () {
var progress = Math.floor(100 * prog.progress)
return hx`<span>${progress}%</span>`
}
function renderTotalProgress () {
var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0)
if (downloaded === total) {
return hx`<span>${downloaded}</span>`
} else {
return hx`<span>${downloaded} / ${total}</span>`
}
}
function renderPeers () {
if (prog.numPeers === 0) return
var count = prog.numPeers === 1 ? 'peer' : 'peers' var count = prog.numPeers === 1 ? 'peer' : 'peers'
return hx`<span>${prog.numPeers} ${count}</span>` return `${prog.numPeers} ${count}`
} }
function renderDownloadSpeed () { function getFilesLength () {
if (prog.downloadSpeed === 0) return if (torrentSummary.files && torrentSummary.files.length > 1) {
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>` return hx`<span class='files'>${torrentSummary.files.length} files</span>`
} }
function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>`
} }
} }
@@ -177,6 +167,7 @@ function TorrentList (state) {
// Show files, per-file download status and play buttons, and so on // Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrentSummary) { function renderTorrentDetails (torrentSummary) {
var infoHash = torrentSummary.infoHash
var filesElement var filesElement
if (!torrentSummary.files) { if (!torrentSummary.files) {
// We don't know what files this torrent contains // We don't know what files this torrent contains
@@ -191,6 +182,10 @@ function TorrentList (state) {
filesElement = hx` filesElement = hx`
<div class='files'> <div class='files'>
<strong>Files</strong> <strong>Files</strong>
<span class='open-folder'
onclick=${dispatcher('openFolder', infoHash)}>
Open folder
</span>
<table> <table>
${fileRows} ${fileRows}
</table> </table>

View File

@@ -1,42 +0,0 @@
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')
function UnsupportedMediaModal (state) {
var err = state.modal.error
var message = (err && err.getMessage)
? err.getMessage()
: err
var actionButton = state.modal.vlcInstalled
? hx`<button class="button-raised" onclick=${onPlay}>Play in VLC</button>`
: hx`<button class="button-raised" onclick=${onInstall}>Install VLC</button>`
var vlcMessage = state.modal.vlcNotFound
? 'Couldn\'t run VLC. Please make sure it\'s installed.'
: ''
return hx`
<div>
<p><strong>Sorry, we can't play that file.</strong></p>
<p>${message}</p>
<p class='float-right'>
<button class="button-flat" onclick=${dispatcher('backToList')}>Cancel</button>
${actionButton}
</p>
<p class='error-text'>${vlcMessage}</p>
</div>
`
function onInstall () {
electron.shell.openExternal('http://www.videolan.org/vlc/')
state.modal.vlcInstalled = true // Assume they'll install it successfully
}
function onPlay () {
dispatch('vlcPlay')
}
}

View File

@@ -28,14 +28,7 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid // Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq // client, as explained here: https://webtorrent.io/faq
var client = window.client = new WebTorrent({ var client = window.client = new WebTorrent()
tracker: {
// HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome bug.
// Fixed in Chrome 51, so we can remove this hack once Electron updates Chrome.
// Issue: https://github.com/feross/webtorrent-desktop/issues/353
wrtc: process.platform !== 'darwin'
}
})
// WebTorrent-to-HTTP streaming sever // WebTorrent-to-HTTP streaming sever
var server = window.server = null var server = window.server = null
@@ -75,20 +68,29 @@ function init () {
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent- // See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
function startTorrenting (torrentKey, torrentID, path, fileModtimes) { function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
console.log('starting torrent %s: %s', torrentKey, torrentID) console.log('starting torrent %s: %s', torrentKey, torrentID)
var torrent
var torrent = client.add(torrentID, { try {
path: path, torrent = client.add(torrentID, {
fileModtimes: fileModtimes path: path,
}) fileModtimes: fileModtimes
})
} catch (err) {
return ipc.send('wt-error', torrentKey, err.message)
}
// If we add a duplicate magnet URI or infohash, WebTorrent returns the
// existing torrent object! (If we add a duplicate torrent file, it creates a
// new torrent object and raises an error later.) Workaround:
if (torrent.key) {
return ipc.send('wt-error', torrentKey, 'Can\'t add duplicate torrent')
}
torrent.key = torrentKey torrent.key = torrentKey
addTorrentEvents(torrent) addTorrentEvents(torrent)
return torrent return torrent
} }
function stopTorrenting (infoHash) { function stopTorrenting (infoHash) {
var torrent = client.get(infoHash) var torrent = client.get(infoHash)
if (torrent) torrent.destroy() torrent.destroy()
} }
// Create a new torrent, start seeding // Create a new torrent, start seeding