Compare commits

..

1 Commits

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

View File

@@ -11,12 +11,5 @@
- Dan Flettre <fletd01@yahoo.com>
- Liam Gray <liam.r.gray@gmail.com>
- 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.

View File

@@ -1,83 +1,16 @@
# WebTorrent Desktop Version History
## v0.5.0 - 2016-05-17
## UNRELEASED
### Added
- Select/deselect individual files to torrent.
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
- "Add Subtitle File..." menu item.
### Changed
- When manually adding subtitle track(s), always switch to the new track.
- Use Squirrel.Windows 1.3.0
- Fix installing when the app is already installed
- Don't kill unrelated processes on uninstall
### Fixed
- Magnet links throw exception on app launch. (OS X)
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
- Missing 'About WebTorrent' menu item. (Windows)
- Rare exception. ("Cannot create BrowserWindow before app is ready")
## v0.4.0 - 2016-05-13
### 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
- Improve app startup time by 40%.
- UI tweaks: Reduce font size, reduce torrent list item height.
- Add Playback menu for playback-related functionality.
- 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
- 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.
Thanks to @grunjol, @rguedes, @furstenheim, @karloluis, @DiegoRBaquero, @alxhotel,
@AgentEpsilon, @remijouannet, Rolando Guedes, @dcposch, and @feross for contributing
to this release!
## v0.3.3 - 2016-04-07
### 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.
*/
var fs = require('fs')
var os = require('os')
var path = require('path')
var pathExists = require('path-exists')
var rimraf = require('rimraf')
var config = require('../config')
@@ -15,12 +15,7 @@ var handlers = require('../main/handlers')
rimraf.sync(config.CONFIG_PATH)
var tmpPath
try {
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
} catch (err) {
tmpPath = path.join(os.tmpDir(), 'webtorrent')
}
var tmpPath = path.join(pathExists.sync('/tmp') ? '/tmp' : os.tmpDir(), 'webtorrent')
rimraf.sync(tmpPath)
// 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 minimist = require('minimist')
var mkdirp = require('mkdirp')
var os = require('os')
var path = require('path')
var rimraf = require('rimraf')
var series = require('run-series')
@@ -19,6 +18,16 @@ var config = require('../config')
var pkg = require('../package.json')
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 argv = minimist(process.argv.slice(2), {
@@ -55,6 +64,9 @@ function build () {
}
var all = {
// Build 64 bit binaries only.
arch: 'x64',
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
// property on Windows, and `NSHumanReadableCopyright` on OS X.
'app-copyright': config.APP_COPYRIGHT,
@@ -73,9 +85,9 @@ var all = {
'asar-unpack': 'WebTorrent*',
// 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
// start with a number. We're using the version of the underlying WebTorrent library.
'build-version': require('webtorrent/package.json').version,
// Windows, and CFBundleVersion on OS X. We're using the short git hash (e.g. 'e7d837e')
// Windows requires the build version to start with a number :/ so we stick on a prefix
'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''),
// The application source directory.
dir: config.ROOT_PATH,
@@ -98,16 +110,12 @@ var all = {
prune: true,
// 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 = {
// Build for OS X
platform: 'darwin',
// Build 64 bit binaries only.
arch: 'x64',
// The bundle identifier to use in the application's plist (OS X only).
'app-bundle-id': 'io.webtorrent.webtorrent',
@@ -123,12 +131,8 @@ var darwin = {
}
var win32 = {
// Build for Windows.
platform: 'win32',
// Build 32 bit binaries only.
arch: 'ia32',
// Object hash of application metadata to embed into the executable (Windows only)
'version-string': {
@@ -157,10 +161,9 @@ var win32 = {
}
var linux = {
// Build for Linux.
platform: 'linux',
// Build 32 and 64 bit binaries.
// Build 32/64 bit binaries.
arch: 'all'
// 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...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
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 contentsPath = path.join(appPath, 'Contents')
@@ -274,7 +277,7 @@ function buildDarwin (cb) {
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
zip.zipSync(inPath, outPath)
zip(inPath, outPath)
console.log('OS X: Created zip.')
}
@@ -324,24 +327,11 @@ function buildDarwin (cb) {
function buildWin32 (cb) {
var installer = require('electron-winstaller')
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) {
if (err) return cb(err)
console.log('Windows: Packaged electron. ' + buildPath)
console.log('Windows: Packaged electron. ' + buildPath[0])
var signWithParams
if (process.platform === 'win32') {
@@ -368,7 +358,6 @@ function buildWin32 (cb) {
function packageInstaller (cb) {
console.log('Windows: Creating installer...')
installer.createWindowsInstaller({
appDirectory: buildPath[0],
authors: config.APP_TEAM,
@@ -387,15 +376,14 @@ function buildWin32 (cb) {
title: config.APP_NAME,
usePackageJson: false,
version: pkg.version
})
.then(function () {
}).then(function () {
console.log('Windows: Created installer.')
cb(null)
})
.catch(cb)
}).catch(cb)
}
function packagePortable (cb) {
// Create Windows portable app
console.log('Windows: Creating portable app...')
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 outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
zip.zipSync(inPath, outPath)
zip(inPath, outPath)
console.log('Windows: Created portable app.')
cb(null)
@@ -415,7 +403,7 @@ function buildLinux (cb) {
console.log('Linux: Packaging electron...')
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
if (err) return cb(err)
console.log('Linux: Packaged electron. ' + buildPath)
console.log('Linux: Packaged electron. ' + buildPath[0])
var tasks = []
buildPath.forEach(function (filesPath) {
@@ -467,7 +455,7 @@ function buildLinux (cb) {
var inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
zip.zipSync(inPath, outPath)
zip(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`)
cb(null)

View File

@@ -6,4 +6,4 @@ npm run package -- --sign
git push
git push --tags
npm publish
./node_modules/.bin/gh-release
gh-release

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
console.time('init')
var electron = require('electron')
var app = electron.app
var ipcMain = electron.ipcMain
var autoUpdater = require('./auto-updater')
var config = require('../config')
var crashReporter = require('../crash-reporter')
var handlers = require('./handlers')
@@ -13,9 +12,8 @@ var log = require('./log')
var menu = require('./menu')
var shortcuts = require('./shortcuts')
var squirrelWin32 = require('./squirrel-win32')
var tray = require('./tray')
var updater = require('./updater')
var windows = require('./windows')
var tray = require('./tray')
var shouldQuit = false
var argv = sliceArgv(process.argv)
@@ -43,7 +41,6 @@ function init () {
app.setPath('userData', config.CONFIG_PATH)
}
var isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false
@@ -55,24 +52,21 @@ function init () {
app.on('will-finish-launching', function () {
crashReporter.init()
autoUpdater.init()
})
app.on('ready', function () {
isReady = true
menu.init()
windows.createMainWindow()
windows.createWebTorrentHiddenWindow()
menu.init()
shortcuts.init()
// To keep app startup fast, some code is delayed.
setTimeout(delayedInit, config.DELAYED_INIT)
tray.init()
handlers.install()
})
app.on('ipcReady', function () {
log('Command line args:', argv)
processArgv(argv)
console.timeEnd('init')
})
app.on('before-quit', function (e) {
@@ -86,16 +80,10 @@ function init () {
})
app.on('activate', function () {
if (isReady) windows.createMainWindow()
windows.createMainWindow()
})
}
function delayedInit () {
tray.init()
handlers.install()
updater.init()
}
function onOpen (e, torrentId) {
e.preventDefault()
@@ -132,11 +120,11 @@ function sliceArgv (argv) {
function processArgv (argv) {
argv.forEach(function (arg) {
if (arg === '-n') {
menu.showOpenSeedFiles()
windows.main.send('dispatch', 'showOpenSeedFiles')
} else if (arg === '-o') {
menu.showOpenTorrentFile()
windows.main.send('dispatch', 'showOpenTorrentFile')
} else if (arg === '-u') {
menu.showOpenTorrentAddress()
windows.main.send('showOpenTorrentAddress')
} else if (arg.startsWith('-psn')) {
// Ignore OS X launchd "process serial number" argument
// More: https://github.com/feross/webtorrent-desktop/issues/214

View File

@@ -6,29 +6,25 @@ var electron = require('electron')
var app = electron.app
var ipcMain = electron.ipcMain
var powerSaveBlocker = electron.powerSaveBlocker
var log = require('./log')
var menu = require('./menu')
var windows = require('./windows')
var shortcuts = require('./shortcuts')
var vlc = require('./vlc')
// has to be a number, not a boolean, and undefined throws an error
var powerSaveBlockerId = 0
// messages from the main process, to be sent once the WebTorrent process starts
var messageQueueMainToWebTorrent = []
// holds a ChildProcess while we're playing a video in VLC, null otherwise
var vlcProcess
var powerSaveBlockID = 0
function init () {
ipcMain.on('ipcReady', function (e) {
windows.main.show()
app.ipcReady = true
app.emit('ipcReady')
windows.main.show()
console.timeEnd('init')
})
var messageQueueMainToWebTorrent = []
ipcMain.on('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true
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('showOpenSeedFiles', menu.showOpenSeedFiles)
ipcMain.on('setBounds', function (e, bounds, maximize) {
setBounds(bounds, maximize)
})
ipcMain.on('setAspectRatio', function (e, aspectRatio) {
setAspectRatio(aspectRatio)
ipcMain.on('setAspectRatio', function (e, aspectRatio, extraSize) {
setAspectRatio(aspectRatio, extraSize)
})
ipcMain.on('setBadge', function (e, text) {
@@ -66,83 +63,26 @@ function init () {
})
ipcMain.on('openItem', function (e, path) {
log('open item: ' + path)
log('opening file or folder: ' + path)
electron.shell.openItem(path)
})
ipcMain.on('showItemInFolder', function (e, path) {
log('show item in folder: ' + path)
electron.shell.showItemInFolder(path)
})
ipcMain.on('blockPowerSave', blockPowerSave)
ipcMain.on('unblockPowerSave', unblockPowerSave)
ipcMain.on('onPlayerOpen', function () {
menu.onPlayerOpen()
shortcuts.onPlayerOpen()
shortcuts.registerPlayerShortcuts()
})
ipcMain.on('onPlayerClose', function () {
menu.onPlayerClose()
shortcuts.onPlayerOpen()
shortcuts.unregisterPlayerShortcuts()
})
ipcMain.on('focusWindow', function (e, windowName) {
windows.focusWindow(windows[windowName])
})
ipcMain.on('downloadFinished', function (e, filePath) {
if (app.dock) {
// Bounces the Downloads stack if the filePath is inside the Downloads folder.
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
var oldEmit = ipcMain.emit
ipcMain.emit = function (name, e, ...args) {
@@ -202,7 +142,8 @@ function setBounds (bounds, maximize) {
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen
var scr = electron.screen.getDisplayMatching(windows.main.getBounds())
var 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.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
log('setBounds: centered to ' + JSON.stringify(bounds))
@@ -213,19 +154,17 @@ function setBounds (bounds, maximize) {
}
}
function setAspectRatio (aspectRatio) {
log('setAspectRatio %o', aspectRatio)
function setAspectRatio (aspectRatio, extraSize) {
log('setAspectRatio %o %o', aspectRatio, extraSize)
if (windows.main) {
windows.main.setAspectRatio(aspectRatio)
windows.main.setAspectRatio(aspectRatio, extraSize)
}
}
// Display string in dock badging area (OS X)
function setBadge (text) {
log('setBadge %s', text)
if (app.dock) {
app.dock.setBadge(String(text))
}
if (app.dock) app.dock.setBadge(String(text))
}
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
@@ -237,13 +176,13 @@ function setProgress (progress) {
}
function blockPowerSave () {
powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep')
log('blockPowerSave %d', powerSaveBlockerId)
powerSaveBlockID = powerSaveBlocker.start('prevent-display-sleep')
log('blockPowerSave %d', powerSaveBlockID)
}
function unblockPowerSave () {
if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) {
electron.powerSaveBlocker.stop(powerSaveBlockerId)
log('unblockPowerSave %d', powerSaveBlockerId)
if (powerSaveBlocker.isStarted(powerSaveBlockID)) {
powerSaveBlocker.stop(powerSaveBlockID)
log('unblockPowerSave %d', powerSaveBlockID)
}
}

View File

@@ -1,14 +1,11 @@
module.exports = {
init,
onPlayerClose,
onPlayerOpen,
onToggleFullScreen,
onWindowHide,
onWindowShow,
// TODO: move these out of menu.js -- they don't belong here
onPlayerOpen,
onPlayerClose,
showOpenSeedFiles,
showOpenTorrentAddress,
showOpenTorrentFile,
toggleFullScreen
}
@@ -21,26 +18,20 @@ var config = require('../config')
var log = require('./log')
var windows = require('./windows')
var appMenu
var appMenu, dockMenu
function init () {
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
electron.Menu.setApplicationMenu(appMenu)
if (app.dock) {
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
app.dock.setMenu(dockMenu)
}
dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
if (app.dock) app.dock.setMenu(dockMenu)
}
function toggleFullScreen (flag) {
log('toggleFullScreen %s', flag)
if (windows.main && windows.main.isVisible()) {
flag = flag != null ? flag : !windows.main.isFullScreen()
if (flag) {
// Allows the window to use the full screen in fullscreen mode (OS X).
windows.main.setAspectRatio(0)
}
windows.main.setFullScreen(flag)
}
}
@@ -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 () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', 0.1)
@@ -86,12 +58,18 @@ function decreaseVolume () {
}
}
function openSubtitles () {
function toggleDevTools () {
log('toggleDevTools')
if (windows.main) {
windows.main.send('dispatch', 'openSubtitles')
windows.main.toggleDevTools()
}
}
function showWebTorrentWindow () {
windows.webtorrent.show()
windows.webtorrent.webContents.openDevTools({ detach: true })
}
function onWindowShow () {
log('onWindowShow')
getMenuItem('Full Screen').enabled = true
@@ -105,19 +83,13 @@ function onWindowHide () {
}
function onPlayerOpen () {
log('onPlayerOpen')
getMenuItem('Play/Pause').enabled = true
getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true
getMenuItem('Add Subtitles File...').enabled = true
}
function onPlayerClose () {
log('onPlayerClose')
getMenuItem('Play/Pause').enabled = false
getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false
getMenuItem('Add Subtitles File...').enabled = false
}
function onToggleFullScreen (isFullScreen) {
@@ -136,29 +108,17 @@ function getMenuItem (label) {
}
}
// Prompts the user for a file, then creates a torrent. Only allows a single file
// selection.
function showOpenSeedFile () {
electron.dialog.showOpenDialog({
title: 'Select a file for the torrent file.',
properties: [ 'openFile' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
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.
// Prompts the user for a file or folder, then makes a torrent out of the data
function showOpenSeedFiles () {
// Allow only a single selection
// To create a multi-file torrent, the user must select a folder
electron.dialog.showOpenDialog({
title: 'Select a file or folder for the torrent file.',
properties: [ 'openFile', 'openDirectory' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
var selectedPath = selectedPaths[0]
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
}, function (filenames) {
if (!Array.isArray(filenames)) return
var fileOrFolder = filenames[0]
windows.main.send('dispatch', 'showCreateTorrent', fileOrFolder)
})
}
@@ -168,10 +128,10 @@ function showOpenTorrentFile () {
title: 'Select a .torrent file to open.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(function (selectedPath) {
windows.main.send('dispatch', 'addTorrent', selectedPath)
}, function (filenames) {
if (!Array.isArray(filenames)) return
filenames.forEach(function (filename) {
windows.main.send('dispatch', 'addTorrent', filename)
})
})
}
@@ -182,38 +142,44 @@ function showOpenTorrentAddress () {
}
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 = [
{
label: 'File',
submenu: [
{
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 === 'win32'
? 'Close'
: 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
submenu: fileMenu
},
{
label: 'Edit',
@@ -259,6 +225,21 @@ function getAppMenuTemplate () {
{
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',
submenu: [
@@ -281,36 +262,13 @@ function getAppMenuTemplate () {
]
},
{
label: 'Playback',
label: 'Window',
role: 'window',
submenu: [
{
label: 'Play/Pause',
accelerator: 'CmdOrCtrl+P',
click: playPause,
enabled: false
},
{
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: 'Add Subtitles File...',
click: openSubtitles,
enabled: false
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
}
]
},
@@ -320,7 +278,7 @@ function getAppMenuTemplate () {
submenu: [
{
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',
@@ -331,14 +289,14 @@ function getAppMenuTemplate () {
},
{
label: 'Report an Issue...',
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES)
click: () => electron.shell.openExternal(config.GITHUB_URL + '/issues')
}
]
}
]
if (process.platform === 'darwin') {
// Add WebTorrent app menu (OS X)
// WebTorrent menu (OS X)
template.unshift({
label: config.APP_NAME,
submenu: [
@@ -382,35 +340,17 @@ function getAppMenuTemplate () {
]
})
// Add Window menu (OS X)
template.splice(5, 0, {
label: 'Window',
role: 'window',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
},
{
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 === 'win32') {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: showOpenSeedFile
})
// Window menu (OS X)
template[4].submenu.push(
{
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
)
} else {
// Help menu (Windows, Linux)
template[4].submenu.push(
{
@@ -422,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
}

View File

@@ -1,34 +1,29 @@
module.exports = {
init,
onPlayerClose,
onPlayerOpen
registerPlayerShortcuts,
unregisterPlayerShortcuts
}
var electron = require('electron')
var localShortcut = require('electron-localshortcut')
var globalShortcut = electron.globalShortcut
var menu = require('./menu')
var windows = require('./windows')
function init () {
var localShortcut = require('electron-localshortcut')
// Alternate shortcuts. Most shortcuts are registered in menu,js, but Electron
// does not support multiple shortcuts for a single menu item.
// ⌘+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
// is registered separately here.
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 () {
// Register special "media key" for play/pause, available on some keyboards
electron.globalShortcut.register(
'MediaPlayPause',
() => windows.main.send('dispatch', 'playPause')
)
function registerPlayerShortcuts () {
// Special "media key" for play/pause, available on some keyboards
globalShortcut.register('MediaPlayPause', () => windows.main.send('dispatch', 'playPause'))
}
function onPlayerClose () {
electron.globalShortcut.unregister('MediaPlayPause')
function unregisterPlayerShortcuts () {
globalShortcut.unregister('MediaPlayPause')
}

View File

@@ -8,6 +8,8 @@ var path = require('path')
var electron = require('electron')
var app = electron.app
var Menu = electron.Menu
var Tray = electron.Tray
var windows = require('./windows')
@@ -33,7 +35,7 @@ function hasTray () {
}
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 Linux, any click (right or left) opens the context menu
@@ -64,7 +66,7 @@ function updateTrayMenu () {
} else {
showHideMenuItem = { label: 'Show', click: showApp }
}
var contextMenu = electron.Menu.buildFromTemplate([
var contextMenu = Menu.buildFromTemplate([
showHideMenuItem,
{ 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 app = electron.app
var config = require('../config')
var menu = require('./menu')
var tray = require('./tray')
@@ -70,7 +68,7 @@ function createWebTorrentHiddenWindow () {
// Prevent killing the WebTorrent process
win.on('close', function (e) {
if (!app.isQuitting) {
if (!electron.app.isQuitting) {
e.preventDefault()
win.hide()
}
@@ -81,9 +79,6 @@ function createWebTorrentHiddenWindow () {
})
}
var HEADER_HEIGHT = 37
var TORRENT_HEIGHT = 100
function createMainWindow () {
if (windows.main) {
return focusWindow(windows.main)
@@ -94,17 +89,14 @@ function createMainWindow () {
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
minWidth: config.WINDOW_MIN_WIDTH,
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,
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
useContentSize: true, // Specify web page size without OS chrome
width: 500,
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
height: 38 + (120 * 5) // header height + 4 torrents
})
win.loadURL(config.WINDOW_MAIN)
if (process.platform === 'darwin') {
win.setSheetOffset(HEADER_HEIGHT)
}
win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen()
@@ -118,8 +110,8 @@ function createMainWindow () {
win.on('close', function (e) {
if (process.platform !== 'darwin' && !tray.hasTray()) {
app.quit()
} else if (!app.isQuitting) {
electron.app.quit()
} else if (!electron.app.isQuitting) {
e.preventDefault()
win.hide()
win.send('dispatch', 'backToList')

View File

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

View File

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

View File

@@ -117,6 +117,10 @@ table {
float: right;
}
.expand-collapse {
cursor: pointer;
}
.expand-collapse.expanded::before {
content: '▲'
}
@@ -272,6 +276,7 @@ table {
}
.modal label {
font-size: 16px;
font-weight: bold;
}
@@ -362,6 +367,7 @@ button { /* Rectangular text buttons */
border-radius: 3px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
color: #aaa;
outline: none;
}
@@ -428,7 +434,7 @@ input {
.torrent,
.torrent-placeholder {
height: 100px;
height: 120px;
}
.torrent:not(:last-child) {
@@ -441,9 +447,9 @@ input {
.torrent .metadata {
position: absolute;
top: 25px;
left: 15px;
right: 15px;
top: 20px;
left: 20px;
right: 20px;
width: calc(100% - 40px);
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
}
@@ -453,15 +459,12 @@ input {
}
.torrent .metadata span:not(:last-child)::after {
content: ' ';
opacity: 0.7;
padding-left: 4px;
padding-right: 4px;
content: ' ';
}
.torrent .buttons {
position: absolute;
top: 29px;
top: 25px;
right: 10px;
align-items: center;
display: none;
@@ -544,11 +547,17 @@ input {
}
.torrent .name {
font-size: 18px;
font-size: 1.5em;
font-weight: bold;
line-height: 1.5em;
}
.torrent .status,
.torrent .status2 {
font-size: 1em;
line-height: 1.5em;
}
/*
* TORRENT LIST: DRAG-DROP TARGET
*/
@@ -592,7 +601,11 @@ body.drag .app::after {
}
.torrent-details {
padding: 8em 12px 20px 20px;
padding: 8em 20px 20px 20px;
}
.torrent-details .open-folder {
float: right;
}
.torrent-details table {
@@ -606,11 +619,8 @@ body.drag .app::after {
height: 28px;
}
.torrent-details td {
vertical-align: center;
}
.torrent-details tr:hover {
.torrent-details tr:hover,
.torrent-details .open-folder:hover {
background-color: rgba(200, 200, 200, 0.3);
}
@@ -620,16 +630,16 @@ body.drag .app::after {
vertical-align: bottom;
}
.torrent-details td .icon {
.torrent-details td.col-icon {
width: 2em;
}
.torrent-details td.col-icon .icon {
font-size: 18px;
position: relative;
top: 3px;
}
.torrent-details td.col-icon {
width: 2em;
}
.torrent-details td.col-name {
width: auto;
text-overflow: ellipsis;
@@ -645,11 +655,6 @@ body.drag .app::after {
text-align: right;
}
.torrent-details td.col-select {
width: 2em;
text-align: right;
}
/*
* PLAYER
*/
@@ -669,9 +674,11 @@ body.drag .app::after {
background-position: center;
}
.player video {
.player .video-player {
display: block;
width: 100%;
height: 100%;
position: relative;
}
/*
@@ -755,15 +762,9 @@ body.drag .app::after {
.player-controls .volume-icon,
.player-controls .back {
display: block;
width: 20px;
height: 20px;
margin: 5px;
/*
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
max-width: 22px;
overflow: hidden;
}
.player-controls .volume,
@@ -809,7 +810,6 @@ body.drag .app::after {
border: none;
padding: 0;
vertical-align: sub;
-webkit-app-region: no-drag;
}
.player-controls .volume-slider::-webkit-slider-thumb {
@@ -820,7 +820,6 @@ body.drag .app::after {
height: 10px;
border: 1px solid #303233;
border-radius: 50%;
-webkit-app-region: no-drag;
}
.player-controls .volume-slider:focus {
@@ -990,7 +989,3 @@ body.drag .app::after {
.error-popover .error:last-child {
border-bottom: none;
}
.error-text {
color: #c44;
}

View File

@@ -1,26 +1,14 @@
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 Async = require('async')
var concat = require('concat-stream')
var dragDrop = require('drag-drop')
var electron = require('electron')
var fs = require('fs-extra')
var iso639 = require('iso-639-1')
var mainLoop = require('main-loop')
var path = require('path')
var srtToVtt = require('srt-to-vtt')
var LanguageDetect = require('languagedetect')
var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff')
@@ -28,6 +16,7 @@ var patch = require('virtual-dom/patch')
var App = require('./views/app')
var config = require('../config')
var crashReporter = require('../crash-reporter')
var errors = require('./lib/errors')
var sound = require('./lib/sound')
var State = require('./state')
@@ -37,7 +26,18 @@ var TorrentSummary = require('./lib/torrent-summary')
var {setDispatch} = require('./lib/dispatcher')
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
var Cast = null
@@ -45,11 +45,12 @@ var Cast = null
// For easy debugging in Developer Tools
var state = global.state = State.getInitialState()
// Push the first page into the location history
state.location.go({ url: 'home' })
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 other state is ephemeral. First we load state.saved then initialize the app.
loadState(init)
@@ -63,11 +64,14 @@ function init () {
// Clean up the freshly-loaded config file, which may be from an older version
cleanUpConfig()
// Push the first page into the location history
state.location.go({ url: 'home' })
// Restart everything we were torrenting last time the app ran
resumeTorrents()
// 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 concepts--one way data flow, a pure function that renders state to a
@@ -92,10 +96,16 @@ function init () {
// ...same thing if you paste a torrent
document.addEventListener('paste', onPaste)
// ...keyboard shortcuts
document.addEventListener('keydown', onKeyDown)
// ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
window.addEventListener('focus', onFocus)
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
sound.play('STARTUP')
@@ -116,19 +126,10 @@ function cleanUpConfig () {
// Migration: replace torrentPath with torrentFileName
var src, dst
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)
if (path.isAbsolute(ts.torrentPath)) {
src = ts.torrentPath
} else if (ts.torrentPath.startsWith('..')) {
src = ts.torrentPath
} else {
src = path.join(config.STATIC_PATH, ts.torrentPath)
}
src = path.isAbsolute(ts.torrentPath)
? ts.torrentPath
: path.join(config.STATIC_PATH, ts.torrentPath)
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
// Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once
@@ -153,11 +154,6 @@ function cleanUpConfig () {
delete ts.posterURL
ts.posterFileName = infoHash + extension
}
// Migration: add per-file selections
if (!ts.selections) {
ts.selections = ts.files.map((x) => true)
}
})
}
@@ -215,6 +211,9 @@ function dispatch (action, ...args) {
if (action === 'addTorrent') {
addTorrent(args[0] /* torrent */)
}
if (action === 'showOpenSeedFiles') {
ipcRenderer.send('showOpenSeedFiles') /* open file or folder to seed */
}
if (action === 'showOpenTorrentFile') {
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
}
@@ -227,6 +226,9 @@ function dispatch (action, ...args) {
if (action === 'openFile') {
openFile(args[0] /* infoHash */, args[1] /* index */)
}
if (action === 'openFolder') {
openFolder(args[0] /* infoHash */)
}
if (action === 'toggleTorrent') {
toggleTorrent(args[0] /* infoHash */)
}
@@ -236,9 +238,6 @@ function dispatch (action, ...args) {
if (action === 'toggleSelectTorrent') {
toggleSelectTorrent(args[0] /* infoHash */)
}
if (action === 'toggleTorrentFile') {
toggleTorrentFile(args[0] /* infoHash */, args[1] /* index */)
}
if (action === 'openTorrentContextMenu') {
openTorrentContextMenu(args[0] /* infoHash */)
}
@@ -252,8 +251,6 @@ function dispatch (action, ...args) {
setDimensions(args[0] /* dimensions */)
}
if (action === 'backToList') {
// Exit any modals and screens with a back button
state.modal = null
while (state.location.hasBack()) state.location.back()
// Work around virtual-dom issue: it doesn't expose its redraw function,
@@ -263,15 +260,6 @@ function dispatch (action, ...args) {
var mediaTag = document.querySelector('video,audio')
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') {
state.location.back()
}
@@ -305,46 +293,29 @@ function dispatch (action, ...args) {
openSubtitles()
}
if (action === 'selectSubtitle') {
selectSubtitle(args[0] /* index */)
selectSubtitle(args[0] /* label */)
}
if (action === 'toggleSubtitlesMenu') {
toggleSubtitlesMenu()
if (action === 'showSubtitles') {
showSubtitles()
}
if (action === 'mediaStalled') {
state.playing.isStalled = true
}
if (action === 'mediaError') {
if (state.location.current().url === 'player') {
state.playing.location = 'error'
ipcRenderer.send('checkForVLC')
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
state.modal = {
id: 'unsupported-media-modal',
error: args[0],
vlcInstalled: isInstalled
}
})
}
state.location.back(function () {
onError(new Error('Unsupported file format'))
})
}
if (action === 'mediaTimeUpdate') {
state.playing.lastTimeUpdate = new Date().getTime()
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') {
ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
}
if (action === 'mediaMouseMoved') {
state.playing.mouseStationarySince = new Date().getTime()
}
if (action === 'exitModal') {
state.modal = null
}
@@ -394,7 +365,6 @@ function pause () {
}
function playPause () {
if (state.location.current().url !== 'player') return
if (state.playing.isPaused) {
play()
} else {
@@ -426,13 +396,13 @@ function setVolume (volume) {
}
function openSubtitles () {
electron.remote.dialog.showOpenDialog({
dialog.showOpenDialog({
title: 'Select a subtitles file.',
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
properties: [ 'openFile' ]
}, function (filenames) {
if (!Array.isArray(filenames)) return
addSubtitles(filenames, true)
addSubtitle({path: filenames[0]})
})
}
@@ -460,10 +430,6 @@ function setupIpc () {
ipcRenderer.on('fullscreenChanged', function (e, 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()
})
@@ -549,23 +515,15 @@ function saveState () {
function onOpen (files) {
if (!Array.isArray(files)) files = [ files ]
// In the player, the only drag-drop function is adding subtitles
var isInPlayer = state.location.current().url === 'player'
if (isInPlayer) {
return addSubtitles(files.filter(isSubtitle), true)
}
// .torrent file = start downloading the torrent
files.filter(isTorrent).forEach(addTorrent)
// Otherwise, you can only drag-drop onto the home screen
var isHome = state.location.current().url === 'home' && !state.modal
if (isHome) {
if (files.every(isTorrent)) {
// All .torrent files? Start downloading
files.forEach(addTorrent)
} else {
// Show the Create Torrent screen. Let's seed those files.
showCreateTorrent(files)
}
}
// subtitle file
files.filter(isSubtitle).forEach(addSubtitle)
// everything else = seed these files
var rest = files.filter(not(isTorrent)).filter(not(isSubtitle))
if (rest.length > 0) showCreateTorrent(rest)
}
function isTorrent (file) {
@@ -581,6 +539,12 @@ function isSubtitle (file) {
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
// Returns undefined if we don't know that infoHash
function getTorrentSummary (torrentKey) {
@@ -601,102 +565,46 @@ function addTorrent (torrentId) {
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
}
function addSubtitles (files, autoSelect) {
// Subtitles are only supported while playing video
function addSubtitle (file) {
if (state.playing.type !== 'video') return
// Read the files concurrently, then add all resulting subtitle tracks
console.log(files)
var subs = state.playing.subtitles
Async.map(files, loadSubtitle, function (err, tracks) {
if (err) return onError(err)
for (var i = 0; i < tracks.length; i++) {
// No dupes allowed
var track = tracks[i]
if (subs.tracks.some((t) => track.filePath === t.filePath)) continue
// Add the track
subs.tracks.push(track)
// If we're auto-selecting a track, try to find one in the user's language
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
state.playing.subtitles.selectedIndex = subs.tracks.length - 1
}
}
// Finally, make sure no two tracks have the same label
relabelSubtitles()
})
}
function loadSubtitle (file, cb) {
var srtToVtt = require('srt-to-vtt')
var LanguageDetect = require('languagedetect')
// Read the .SRT or .VTT file, parse it, add subtitle track
var filePath = file.path || file
fs.createReadStream(filePath).pipe(srtToVtt()).pipe(concat(function (buf) {
// Detect what language the subtitles are in
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
fs.createReadStream(file.path || file).pipe(srtToVtt()).pipe(concat(function (buf) {
// Set the cue text position so it appears above the player controls.
// The only way to change cue text position is by modifying the VTT. It is not
// possible via CSS.
var langDetected = (new LanguageDetect()).detect(buf.toString().replace(/(.*-->.*)/g, ''), 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
var track = {
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
language: langDetected,
label: langDetected,
filePath: filePath
selected: true
}
cb(null, track)
state.playing.subtitles.tracks.forEach(function (trackItem) {
trackItem.selected = false
if (trackItem.label === track.label) {
track.label = Number.isNaN(track.label.slice(-1))
? track.label + ' 2'
: track.label.slice(0, -1) + (parseInt(track.label.slice(-1)) + 1)
}
})
state.playing.subtitles.change = track.label
state.playing.subtitles.tracks.push(track)
state.playing.subtitles.enabled = true
}))
}
function selectSubtitle (ix) {
state.playing.subtitles.selectedIndex = ix
}
// Checks whether a language name like "English" or "German" matches the system
// language, aka the current locale
function isSystemLanguage (language) {
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
var langIso = iso639.getCode(language) // eg "de" if language is "German"
return langIso === osLangISO
}
// Make sure we don't have two subtitle tracks with the same label
// Labels each track by language, eg "German", "English", "English 2", ...
function relabelSubtitles () {
var counts = {}
function selectSubtitle (label) {
state.playing.subtitles.tracks.forEach(function (track) {
var lang = track.language
counts[lang] = (counts[lang] || 0) + 1
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
track.selected = (track.label === label)
})
state.playing.subtitles.enabled = !!label
state.playing.subtitles.change = label
state.playing.subtitles.show = false
}
function checkForSubtitles () {
if (state.playing.type !== 'video') return
var torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary || !torrentSummary.progress) return
torrentSummary.progress.files.forEach(function (fp, ix) {
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
var file = torrentSummary.files[ix]
if (!isSubtitle(file.name)) return
var filePath = path.join(torrentSummary.path, file.path)
addSubtitles([filePath], false)
})
}
function toggleSubtitlesMenu () {
state.playing.subtitles.showMenu = !state.playing.subtitles.showMenu
function showSubtitles () {
state.playing.subtitles.show = !state.playing.subtitles.show
}
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
@@ -716,8 +624,7 @@ function startTorrentingSummary (torrentSummary) {
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, s.selections)
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes)
}
//
@@ -803,16 +710,16 @@ function torrentWarning (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)
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'
update()
}
@@ -826,9 +733,6 @@ function torrentMetadata (torrentKey, torrentInfo) {
torrentSummary.path = torrentInfo.path
torrentSummary.files = torrentInfo.files
torrentSummary.magnetURI = torrentInfo.magnetURI
if (!torrentSummary.selections) {
torrentSummary.selections = torrentSummary.files.map((x) => true)
}
update()
// Save the .torrent file, if it hasn't been saved already
@@ -850,7 +754,6 @@ function torrentDone (torrentKey, torrentInfo) {
state.dock.badge += 1
}
showDoneNotification(torrentSummary)
ipcRenderer.send('downloadFinished', getTorrentPath(torrentSummary))
}
update()
@@ -880,8 +783,6 @@ function torrentProgress (progressInfo) {
torrentSummary.progress = p
})
checkForSubtitles()
update()
}
@@ -981,9 +882,6 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
}
// if it's video, check for subtitles files that are done downloading
checkForSubtitles()
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, function (e, info) {
clearTimeout(timeout)
@@ -1010,9 +908,6 @@ function closePlayer (cb) {
if (isCasting()) {
Cast.close()
}
if (state.playing.location === 'vlc') {
ipcRenderer.send('vlcQuit')
}
state.window.title = config.APP_WINDOW_TITLE
state.playing = State.getDefaultPlayState()
state.server = null
@@ -1038,6 +933,17 @@ function openFile (infoHash, index) {
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
function toggleTorrent (infoHash) {
var torrentSummary = getTorrentSummary(infoHash)
@@ -1069,56 +975,25 @@ function toggleSelectTorrent (infoHash) {
update()
}
function toggleTorrentFile (infoHash, index) {
var torrentSummary = getTorrentSummary(infoHash)
torrentSummary.selections[index] = !torrentSummary.selections[index]
// Let the WebTorrent process know to start or stop fetching that file
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
}
function openTorrentContextMenu (infoHash) {
var torrentSummary = getTorrentSummary(infoHash)
var menu = new electron.remote.Menu()
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({
var menu = new Menu()
menu.append(new MenuItem({
label: 'Save Torrent File As...',
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) {
var itemPath = path.join(torrentSummary.path, torrentSummary.files[0].path)
if (torrentSummary.files.length > 1) {
itemPath = path.dirname(itemPath)
}
return itemPath
}
menu.append(new MenuItem({
label: 'Copy Magnet Link to Clipboard',
click: () => clipboard.writeText(torrentSummary.magnetURI)
}))
function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
menu.popup(remote.getCurrentWindow())
}
function saveTorrentFileAs (torrentSummary) {
@@ -1131,7 +1006,7 @@ function saveTorrentFileAs (torrentSummary) {
{ 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)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return onError(err)
@@ -1145,7 +1020,7 @@ function saveTorrentFileAs (torrentSummary) {
// Set window dimensions to match video dimensions or fill the screen
function setDimensions (dimensions) {
// Don't modify the window size if it's already maximized
if (electron.remote.getCurrentWindow().isMaximized()) {
if (remote.getCurrentWindow().isMaximized()) {
state.window.bounds = null
return
}
@@ -1157,7 +1032,7 @@ function setDimensions (dimensions) {
width: window.outerWidth,
height: window.outerHeight
}
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
state.window.wasMaximized = remote.getCurrentWindow().isMaximized
// Limit window size to screen size
var screenWidth = window.screen.width
@@ -1178,7 +1053,6 @@ function setDimensions (dimensions) {
ipcRenderer.send('setAspectRatio', aspectRatio)
ipcRenderer.send('setBounds', {x: null, y: null, width, height})
state.playing.aspectRatio = aspectRatio
}
function restoreBounds () {
@@ -1238,7 +1112,7 @@ function onWarning (err) {
function onPaste (e) {
if (e.target.tagName.toLowerCase() === 'input') return
var torrentIds = electron.clipboard.readText().split('\n')
var torrentIds = clipboard.readText().split('\n')
torrentIds.forEach(function (torrentId) {
torrentId = torrentId.trim()
if (torrentId.length === 0) return
@@ -1246,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) {
state.window.isFocused = true
state.dock.badge = 0

View File

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

View File

@@ -6,43 +6,41 @@ module.exports = {
var config = require('../../config')
var path = require('path')
var VOLUME = 0.15
/* Cache of Audio elements, for instant playback */
var cache = {}
var sounds = {
ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
volume: VOLUME
volume: 0.2
},
DELETE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
volume: VOLUME
volume: 0.1
},
DISABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
volume: VOLUME
volume: 0.2
},
DONE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
volume: VOLUME
volume: 0.2
},
ENABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
volume: VOLUME
volume: 0.2
},
ERROR: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
volume: VOLUME
volume: 0.2
},
PLAY: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
volume: VOLUME
volume: 0.2
},
STARTUP: {
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) {
var ext = path.extname(file.name).toLowerCase()
return [
'.avi',
'.m4v',
'.mkv',
'.mov',
'.mp4',
'.mpg',
'.ogv',
'.webm'
].includes(ext)
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
}
function isAudio (file) {
var ext = path.extname(file.name).toLowerCase()
return [
'.aac',
'.ac3',
'.mp3',
'.ogg',
'.wav'
].includes(ext)
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
}
function isPlayableTorrent (torrentSummary) {

View File

@@ -9,8 +9,7 @@ var LocationHistory = require('./lib/location-history')
module.exports = {
getInitialState,
getDefaultPlayState,
getDefaultSavedState,
getPlayingTorrentSummary
getDefaultSavedState
}
function getInitialState () {
@@ -58,12 +57,7 @@ function getInitialState () {
*
* Also accessible via `require('application-config')('WebTorrent').filePath`
*/
saved: {},
/*
* Getters, for convenience
*/
getPlayingTorrentSummary
saved: {}
}
}
@@ -81,11 +75,9 @@ function getDefaultPlayState () {
lastTimeUpdate: 0, /* Unix time in ms */
mouseStationarySince: 0, /* Unix time in ms */
subtitles: {
tracks: [], /* subtitle tracks, each {label, language, ...} */
selectedIndex: -1, /* current subtitle track */
showMenu: false /* popover menu, above the video */
},
aspectRatio: 0 /* aspect ratio of the video */
tracks: [], /* subtitles file (Buffer) */
enabled: false
}
}
}
@@ -270,8 +262,3 @@ function getDefaultSavedState () {
: remote.app.getPath('downloads')
}
}
function getPlayingTorrentSummary () {
var infoHash = this.playing.infoHash
return this.saved.torrents.find((x) => x.infoHash === infoHash)
}

View File

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

View File

@@ -33,6 +33,7 @@ function CreateTorrentPage (state) {
// Sanity check: show the number of files and total size
var numFiles = files.length
console.log('FILES', files)
var totalBytes = files
.map((f) => f.size)
.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)
// as the default name. Show all files relative to the base folder.
var defaultName, basePath
if (files.length === 1) {
// 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 = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
var defaultName = path.basename(pathPrefix)
var basePath = path.dirname(pathPrefix)
var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file) {
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 hx = hyperx(h)
var WebChimeraPlayer = require('wcjs-player')
var prettyBytes = require('prettier-bytes')
var Bitfield = require('bitfield')
@@ -18,7 +19,6 @@ function Player (state) {
return hx`
<div
class='player'
onwheel=${handleVolumeWheel}
onmousemove=${dispatcher('mediaMouseMoved')}>
${showVideo ? renderMedia(state) : renderCastScreen(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) {
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.
// Instead, grab the DOM node and play/pause it if necessary
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
@@ -54,10 +56,14 @@ function renderMedia (state) {
state.playing.setVolume = null
}
// Switch to the newly added subtitle track, if available
var tracks = mediaElement.textTracks
for (var j = 0; j < tracks.length; j++) {
tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden'
// fix textTrack cues not been removed <track> rerender
if (state.playing.subtitles.change) {
var tracks = mediaElement.textTracks
for (var j = 0; j < tracks.length; j++) {
// mode is not an <track> attribute, only available on DOM
tracks[j].mode = (tracks[j].label === state.playing.subtitles.change) ? 'showing' : 'hidden'
}
state.playing.subtitles.change = null
}
state.playing.currentTime = mediaElement.currentTime
@@ -67,13 +73,12 @@ function renderMedia (state) {
// Add subtitles to the <video> tag
var trackTags = []
if (state.playing.subtitles.selectedIndex >= 0) {
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]
var isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push(hx`
<track
${isSelected ? 'default' : ''}
${track.selected ? 'default' : ''}
label=${track.label}
type='subtitles'
src=${track.buffer}>
@@ -91,8 +96,7 @@ function renderMedia (state) {
onstalling=${dispatcher('mediaStalled')}
onerror=${dispatcher('mediaError')}
ontimeupdate=${dispatcher('mediaTimeUpdate')}
onencrypted=${dispatcher('mediaEncrypted')}
oncanplay=${onCanPlay}>
autoplay>
${trackTags}
</div>
`
@@ -123,15 +127,101 @@ function renderMedia (state) {
function onEnded (e) {
state.playing.isPaused = true
}
}
function onCanPlay (e) {
var video = e.target
if (video.webkitVideoDecodedByteCount > 0 &&
video.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported')
// Renders using WebChimera.js to render using VLC
// That lets us play media that the <video> tag can't play
function renderMediaVLC (state) {
// Unfortunately, WebChimera can't be done just by modifying HTML.
// 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 {
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
}
}
@@ -161,7 +251,7 @@ function renderOverlay (state) {
}
function renderAudioMetadata (state) {
var torrentSummary = state.getPlayingTorrentSummary()
var torrentSummary = getPlayingTorrentSummary(state)
var fileSummary = torrentSummary.files[state.playing.fileIndex]
if (!fileSummary.audioInfo) return
var info = fileSummary.audioInfo
@@ -200,7 +290,7 @@ function renderLoadingSpinner (state) {
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
if (!isProbablyStalled) return
var prog = state.getPlayingTorrentSummary().progress || {}
var prog = getPlayingTorrentSummary(state).progress || {}
var fileProgress = 0
if (prog.files) {
var file = prog.files[state.playing.fileIndex]
@@ -220,33 +310,20 @@ function renderLoadingSpinner (state) {
}
function renderCastScreen (state) {
var castIcon, castType, isCast
var castIcon, castType
if (state.playing.location.startsWith('chromecast')) {
castIcon = 'cast_connected'
castType = 'Chromecast'
isCast = true
} else if (state.playing.location.startsWith('airplay')) {
castIcon = 'airplay'
castType = 'AirPlay'
isCast = true
} else if (state.playing.location.startsWith('dlna')) {
castIcon = 'tv'
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 castStatus
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
else castStatus = ''
var castStatus = isStarting ? 'Connecting...' : 'Connected'
// Show a nice title image, if possible
var style = {
@@ -266,28 +343,15 @@ function renderCastScreen (state) {
function renderSubtitlesOptions (state) {
var subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return
var items = subtitles.tracks.map(function (track, ix) {
var isSelected = state.playing.subtitles.selectedIndex === ix
return hx`
<li onclick=${dispatcher('selectSubtitle', ix)}>
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
${track.label}
</li>
if (subtitles.tracks.length && subtitles.show) {
return hx`<ul.subtitles-list>
${subtitles.tracks.map(function (w, i) {
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', '')}><i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>None</li>
</ul>
`
})
var noneSelected = state.playing.subtitles.selectedIndex === -1
return hx`
<ul.subtitles-list>
${items}
<li onclick=${dispatcher('selectSubtitle', -1)}>
<i.icon>${noneSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
None
</li>
</ul>
`
}
}
function renderPlayerControls (state) {
@@ -295,7 +359,7 @@ function renderPlayerControls (state) {
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled'
: state.playing.subtitles.selectedIndex >= 0
: state.playing.subtitles.enabled
? 'active'
: ''
@@ -412,7 +476,8 @@ function renderPlayerControls (state) {
}
elements.push(hx`
<div.volume>
<div.volume
onwheel=${handleVolumeWheel}>
<i.icon.volume-icon onmousedown=${handleVolumeMute}>
${volumeIcon}
</i>
@@ -421,6 +486,7 @@ function renderPlayerControls (state) {
onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub}
onwheel=${handleVolumeWheel}
style=${volumeStyle}
/>
</div>
@@ -449,6 +515,11 @@ function renderPlayerControls (state) {
dispatch('playbackJump', position)
}
// Handles volume change by wheel
function handleVolumeWheel (e) {
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
}
// Handles volume muting and Unmuting
function handleVolumeMute (e) {
if (state.playing.volume === 0.0) {
@@ -482,7 +553,7 @@ function renderPlayerControls (state) {
// if no subtitles available select it
dispatch('openSubtitles')
} else {
dispatch('toggleSubtitlesMenu')
dispatch('showSubtitles')
}
}
}
@@ -493,7 +564,7 @@ var volumeChanging = false
// Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be "spongey" / non-contiguous
function renderLoadingBar (state) {
var torrentSummary = state.getPlayingTorrentSummary()
var torrentSummary = getPlayingTorrentSummary(state)
if (!torrentSummary.progress) {
return []
}
@@ -530,7 +601,7 @@ function renderLoadingBar (state) {
// Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) {
var torrentSummary = state.getPlayingTorrentSummary()
var torrentSummary = getPlayingTorrentSummary(state)
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
if (!posterPath) return ''
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
@@ -540,3 +611,8 @@ function cssBackgroundImageDarkGradient () {
return 'radial-gradient(circle at center, ' +
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
}
function getPlayingTorrentSummary (state) {
var infoHash = state.playing.infoHash
return state.saved.torrents.find((x) => x.infoHash === infoHash)
}

View File

@@ -67,48 +67,38 @@ function TorrentList (state) {
// If it's downloading/seeding then show progress info
var prog = torrentSummary.progress
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`
<div class='ellipsis'>
${renderPercentProgress()}
${renderTotalProgress()}
${renderPeers()}
${renderDownloadSpeed()}
${renderUploadSpeed()}
<div class='status ellipsis'>
${getFilesLength()}
<span>${getPeers()}</span>
<span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
<span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
</div>
`)
elements.push(hx`
<div class='status2 ellipsis'>
<span class='progress'>${progress}%</span>
<span>${downloaded}</span>
</div>
`)
}
return hx`<div class='metadata'>${elements}</div>`
function renderPercentProgress () {
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
function getPeers () {
var count = prog.numPeers === 1 ? 'peer' : 'peers'
return hx`<span>${prog.numPeers} ${count}</span>`
return `${prog.numPeers} ${count}`
}
function renderDownloadSpeed () {
if (prog.downloadSpeed === 0) return
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>`
}
function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>`
function getFilesLength () {
if (torrentSummary.files && torrentSummary.files.length > 1) {
return hx`<span class='files'>${torrentSummary.files.length} files</span>`
}
}
}
@@ -177,6 +167,7 @@ function TorrentList (state) {
// Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrentSummary) {
var infoHash = torrentSummary.infoHash
var filesElement
if (!torrentSummary.files) {
// We don't know what files this torrent contains
@@ -191,6 +182,10 @@ function TorrentList (state) {
filesElement = hx`
<div class='files'>
<strong>Files</strong>
<span class='open-folder'
onclick=${dispatcher('openFolder', infoHash)}>
Open folder
</span>
<table>
${fileRows}
</table>
@@ -208,8 +203,7 @@ function TorrentList (state) {
// Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded
var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
var isDone = false // Are we finished torrenting it?
var isDone = false
var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files) {
var fileProg = torrentSummary.progress.files[index]
@@ -218,38 +212,26 @@ function TorrentList (state) {
}
// Second, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash
var icon
var rowClass = ''
var handleClick
if (isPlayable) {
if (TorrentPlayer.isPlayable(file)) {
icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index)
} else {
icon = 'description' /* file icon, opens in OS default app */
rowClass = isDone ? '' : 'disabled'
handleClick = dispatcher('openFile', infoHash, index)
}
var rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return hx`
<tr>
<td class='col-icon ${rowClass}' onclick=${handleClick}>
<tr onclick=${handleClick} class='${rowClass}'>
<td class='col-icon'>
<i class='icon'>${icon}</i>
</td>
<td class='col-name ${rowClass}' onclick=${handleClick}>
${file.name}
</td>
<td class='col-progress ${rowClass}' onclick=${handleClick}>
${isSelected ? progress : ''}
</td>
<td class='col-size ${rowClass}' onclick=${handleClick}>
${prettyBytes(file.length)}
</td>
<td class='col-select'
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
</td>
<td class='col-name'>${file.name}</td>
<td class='col-progress'>${progress}</td>
<td class='col-size'>${prettyBytes(file.length)}</td>
</tr>
`
}

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
// client, as explained here: https://webtorrent.io/faq
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'
}
})
var client = window.client = new WebTorrent()
// WebTorrent-to-HTTP streaming sever
var server = window.server = null
@@ -49,8 +42,8 @@ function init () {
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message))
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes))
ipc.on('wt-stop-torrenting', (e, infoHash) =>
stopTorrenting(infoHash))
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
@@ -65,8 +58,6 @@ function init () {
startServer(infoHash, index))
ipc.on('wt-stop-server', (e) =>
stopServer())
ipc.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections))
ipc.send('ipcReadyWebTorrent')
@@ -75,27 +66,31 @@ function init () {
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
console.log('starting torrent %s: %s', torrentKey, torrentID)
var torrent = client.add(torrentID, {
path: path,
fileModtimes: fileModtimes
})
var torrent
try {
torrent = client.add(torrentID, {
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
// Listen for ready event, progress notifications, etc
addTorrentEvents(torrent)
// Only download the files the user wants, not necessarily all files
torrent.once('ready', () => selectFiles(torrent, selections))
return torrent
}
function stopTorrenting (infoHash) {
var torrent = client.get(infoHash)
if (torrent) torrent.destroy()
torrent.destroy()
}
// Create a new torrent, start seeding
@@ -164,7 +159,9 @@ function getTorrentFileInfo (file) {
return {
name: file.name,
length: file.length,
path: file.path
path: file.path,
numPiecesPresent: 0,
numPieces: null
}
}
@@ -315,48 +312,6 @@ function getAudioMetadata (infoHash, index) {
})
}
function selectFiles (torrentOrInfoHash, selections) {
// Get the torrent object
var torrent
if (typeof torrentOrInfoHash === 'string') {
torrent = client.get(torrentOrInfoHash)
} else {
torrent = torrentOrInfoHash
}
// Selections not specified?
// Load all files. We still need to replace the default whole-torrent
// selection with individual selections for each file, so we can
// select/deselect files later on
if (!selections) {
selections = torrent.files.map((x) => true)
}
// Selections specified incorrectly?
if (selections.length !== torrent.files.length) {
throw new Error('got ' + selections.length + ' file selections, ' +
'but the torrent contains ' + torrent.files.length + ' files')
}
// Remove default selection (whole torrent)
torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files)
for (var i = 0; i < selections.length; i++) {
var file = torrent.files[i]
if (selections[i]) {
file.select()
} else {
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
file.deselect()
// If we deselected a file, try to nuke it to save disk space
var filePath = path.join(torrent.path, file.path)
fs.unlink(filePath) // Ignore errors for now
}
}
}
// Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) {