Compare commits

..

1 Commits

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

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
node_modules node_modules
dist dist

View File

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

View File

@@ -1,118 +1,16 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.6.0 - 2016-05-24 ## UNRELEASED
### Added ### Added
- Added Preferences page
- Save video position, resume playback from saved position
- Add additional video player keyboard shortcuts (#275)
- Use `poster.jpg` file as the poster image if available (#558)
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
- Add support for pasting a `instant.io` links (#559)
- Add announcement feature
### Changed ### Changed
- Nicer player UI - Use Squirrel.Windows 1.3.0
- Reduce startup jank, improve startup time (#568) - Fix installing when the app is already installed
- Cleanup unsupported codec detection (#569, #570) - Don't kill unrelated processes on uninstall
- Cleaner look for the torrent file list
- Improve subtitle positioning (#551)
### Fixed ### Fixed
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
- Fix bugs in LocationHistory
- When player is active, and magnet link is pasted, go back to list
- After deleting torrent, remove just the player from forward stack
- After creating torrent, remove create torrent page from forward stack
- Cancel button on create torrent page should only go back one page
## v0.5.1 - 2016-05-18
### Fixed
- Fix auto-updater (OS X, Windows).
## v0.5.0 - 2016-05-17
### 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.
### 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 ## v0.3.3 - 2016-04-07
### Fixed ### Fixed

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) WebTorrent, LLC Copyright (c) Feross Aboukhadijeh
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@@ -87,4 +87,4 @@ brew install wine
## License ## License
MIT. Copyright (c) [WebTorrent, LLC](https://webtorrent.io). MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org).

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env node
var fs = require('fs')
var cp = require('child_process')
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen']
var EXECUTABLE_DEPS = ['gh-release', 'standard']
main()
// Scans our codebase and package.json for missing or unused dependencies
// Process returns 0 on success, prints a message and returns 1 on failure
function main () {
if (process.platform === 'win32') {
console.log('Sorry, check-deps only works on Mac and Linux')
return
}
var jsDeps = findJSDeps()
var packageDeps = findPackageDeps()
var missingDeps = jsDeps.filter((dep) =>
packageDeps.indexOf(dep) < 0 &&
BUILT_IN_DEPS.indexOf(dep) < 0)
var unusedDeps = packageDeps.filter((dep) =>
jsDeps.indexOf(dep) < 0 &&
EXECUTABLE_DEPS.indexOf(dep) < 0)
if (missingDeps.length > 0) console.log('Missing package dependencies: ' + missingDeps)
if (unusedDeps.length > 0) console.log('Unused package dependencies: ' + unusedDeps)
if (missingDeps.length + unusedDeps.length > 0) process.exit(1)
console.log('Lookin good!')
}
// Finds all dependencies, required, optional, or dev, in package.json
function findPackageDeps () {
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
var requiredDeps = Object.keys(pkg.dependencies)
var devDeps = Object.keys(pkg.devDependencies)
var optionalDeps = Object.keys(pkg.optionalDependencies)
return [].concat(requiredDeps, devDeps, optionalDeps)
}
// Finds all dependencies required() in the code
function findJSDeps () {
var stdout = cp.execSync('./bin/list-deps.sh')
return stdout.toString().trim().split('\n')
}

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ var electronPackager = require('electron-packager')
var fs = require('fs') var fs = require('fs')
var minimist = require('minimist') var minimist = require('minimist')
var mkdirp = require('mkdirp') var mkdirp = require('mkdirp')
var os = require('os')
var path = require('path') var path = require('path')
var rimraf = require('rimraf') var rimraf = require('rimraf')
var series = require('run-series') var series = require('run-series')
@@ -19,6 +18,16 @@ var config = require('../config')
var pkg = require('../package.json') var pkg = require('../package.json')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
/*
* Path to folder with the following files:
* - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt)
*/
var CERT_PATH = process.platform === 'win32'
? 'D:'
: '/Volumes/Certs'
var DIST_PATH = path.join(config.ROOT_PATH, 'dist') var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
var argv = minimist(process.argv.slice(2), { var argv = minimist(process.argv.slice(2), {
@@ -55,6 +64,9 @@ function build () {
} }
var all = { var all = {
// Build 64 bit binaries only.
arch: 'x64',
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata // The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
// property on Windows, and `NSHumanReadableCopyright` on OS X. // property on Windows, and `NSHumanReadableCopyright` on OS X.
'app-copyright': config.APP_COPYRIGHT, 'app-copyright': config.APP_COPYRIGHT,
@@ -73,9 +85,9 @@ var all = {
'asar-unpack': 'WebTorrent*', 'asar-unpack': 'WebTorrent*',
// The build version of the application. Maps to the FileVersion metadata property on // The build version of the application. Maps to the FileVersion metadata property on
// Windows, and CFBundleVersion on OS X. Note: Windows requires the build version to // Windows, and CFBundleVersion on OS X. We're using the short git hash (e.g. 'e7d837e')
// start with a number. We're using the version of the underlying WebTorrent library. // Windows requires the build version to start with a number :/ so we stick on a prefix
'build-version': require('webtorrent/package.json').version, 'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''),
// The application source directory. // The application source directory.
dir: config.ROOT_PATH, dir: config.ROOT_PATH,
@@ -98,16 +110,12 @@ var all = {
prune: true, prune: true,
// The Electron version with which the app is built (without the leading 'v') // The Electron version with which the app is built (without the leading 'v')
version: require('electron-prebuilt/package.json').version version: pkg.dependencies['electron-prebuilt']
} }
var darwin = { var darwin = {
// Build for OS X
platform: 'darwin', platform: 'darwin',
// Build 64 bit binaries only.
arch: 'x64',
// The bundle identifier to use in the application's plist (OS X only). // The bundle identifier to use in the application's plist (OS X only).
'app-bundle-id': 'io.webtorrent.webtorrent', 'app-bundle-id': 'io.webtorrent.webtorrent',
@@ -123,12 +131,8 @@ var darwin = {
} }
var win32 = { var win32 = {
// Build for Windows.
platform: 'win32', platform: 'win32',
// Build 32 bit binaries only.
arch: 'ia32',
// Object hash of application metadata to embed into the executable (Windows only) // Object hash of application metadata to embed into the executable (Windows only)
'version-string': { 'version-string': {
@@ -157,10 +161,9 @@ var win32 = {
} }
var linux = { var linux = {
// Build for Linux.
platform: 'linux', platform: 'linux',
// Build 32 and 64 bit binaries. // Build 32/64 bit binaries.
arch: 'all' arch: 'all'
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option. // Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
@@ -174,7 +177,7 @@ function buildDarwin (cb) {
console.log('OS X: Packaging electron...') console.log('OS X: Packaging electron...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('OS X: Packaged electron. ' + buildPath) console.log('OS X: Packaged electron. ' + buildPath[0])
var appPath = path.join(buildPath[0], config.APP_NAME + '.app') var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
var contentsPath = path.join(appPath, 'Contents') var contentsPath = path.join(appPath, 'Contents')
@@ -182,6 +185,8 @@ function buildDarwin (cb) {
var infoPlistPath = path.join(contentsPath, 'Info.plist') var infoPlistPath = path.join(contentsPath, 'Info.plist')
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
// TODO: Use new `extend-info` and `extra-resource` opts to electron-packager,
// available as of v6.
infoPlist.CFBundleDocumentTypes = [ infoPlist.CFBundleDocumentTypes = [
{ {
CFBundleTypeExtensions: [ 'torrent' ], CFBundleTypeExtensions: [ 'torrent' ],
@@ -209,25 +214,6 @@ function buildDarwin (cb) {
} }
] ]
infoPlist.UTExportedTypeDeclarations = [
{
UTTypeConformsTo: [
'public.data',
'public.item',
'com.bittorrent.torrent'
],
UTTypeDescription: 'BitTorrent Document',
UTTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
UTTypeIdentifier: 'org.bittorrent.torrent',
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
UTTypeTagSpecification: {
'com.apple.ostype': 'TORR',
'public.filename-extension': [ 'torrent' ],
'public.mime-type': 'application/x-bittorrent'
}
}
]
fs.writeFileSync(infoPlistPath, plist.build(infoPlist)) fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
// Copy torrent file icon into app bundle // Copy torrent file icon into app bundle
@@ -291,7 +277,7 @@ function buildDarwin (cb) {
var inPath = path.join(buildPath[0], config.APP_NAME + '.app') var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
zip.zipSync(inPath, outPath) zip(inPath, outPath)
console.log('OS X: Created zip.') console.log('OS X: Created zip.')
} }
@@ -341,24 +327,11 @@ function buildDarwin (cb) {
function buildWin32 (cb) { function buildWin32 (cb) {
var installer = require('electron-winstaller') var installer = require('electron-winstaller')
console.log('Windows: Packaging electron...') console.log('Windows: Packaging electron...')
/*
* Path to folder with the following files:
* - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt)
*/
var CERT_PATH
try {
fs.accessSync('D:')
CERT_PATH = 'D:'
} catch (err) {
CERT_PATH = path.join(os.homedir(), 'Desktop')
}
electronPackager(Object.assign({}, all, win32), function (err, buildPath) { electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('Windows: Packaged electron. ' + buildPath) console.log('Windows: Packaged electron. ' + buildPath[0])
var signWithParams var signWithParams
if (process.platform === 'win32') { if (process.platform === 'win32') {
@@ -385,7 +358,6 @@ function buildWin32 (cb) {
function packageInstaller (cb) { function packageInstaller (cb) {
console.log('Windows: Creating installer...') console.log('Windows: Creating installer...')
installer.createWindowsInstaller({ installer.createWindowsInstaller({
appDirectory: buildPath[0], appDirectory: buildPath[0],
authors: config.APP_TEAM, authors: config.APP_TEAM,
@@ -404,15 +376,14 @@ function buildWin32 (cb) {
title: config.APP_NAME, title: config.APP_NAME,
usePackageJson: false, usePackageJson: false,
version: pkg.version version: pkg.version
}) }).then(function () {
.then(function () {
console.log('Windows: Created installer.') console.log('Windows: Created installer.')
cb(null) cb(null)
}) }).catch(cb)
.catch(cb)
} }
function packagePortable (cb) { function packagePortable (cb) {
// Create Windows portable app
console.log('Windows: Creating portable app...') console.log('Windows: Creating portable app...')
var portablePath = path.join(buildPath[0], 'Portable Settings') var portablePath = path.join(buildPath[0], 'Portable Settings')
@@ -420,7 +391,7 @@ function buildWin32 (cb) {
var inPath = path.join(DIST_PATH, path.basename(buildPath[0])) var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
zip.zipSync(inPath, outPath) zip(inPath, outPath)
console.log('Windows: Created portable app.') console.log('Windows: Created portable app.')
cb(null) cb(null)
@@ -432,7 +403,7 @@ function buildLinux (cb) {
console.log('Linux: Packaging electron...') console.log('Linux: Packaging electron...')
electronPackager(Object.assign({}, all, linux), function (err, buildPath) { electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('Linux: Packaged electron. ' + buildPath) console.log('Linux: Packaged electron. ' + buildPath[0])
var tasks = [] var tasks = []
buildPath.forEach(function (filesPath) { buildPath.forEach(function (filesPath) {
@@ -484,7 +455,7 @@ function buildLinux (cb) {
var inPath = path.join(DIST_PATH, path.basename(filesPath)) var inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
zip.zipSync(inPath, outPath) zip(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`) console.log(`Linux: Created ${destArch} zip.`)
cb(null) cb(null)

View File

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

View File

@@ -6,5 +6,6 @@ npm run update-authors
git diff --exit-code git diff --exit-code
rm -rf node_modules/ rm -rf node_modules/
npm install npm install
npm prune
npm dedupe npm dedupe
npm test npm test

View File

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

View File

@@ -1,16 +1,14 @@
var appConfig = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
var fs = require('fs')
var path = require('path') var path = require('path')
var pathExists = require('path-exists')
var APP_NAME = 'WebTorrent' var APP_NAME = 'WebTorrent'
var APP_TEAM = 'WebTorrent, LLC' var APP_TEAM = 'The WebTorrent Project'
var APP_VERSION = require('./package.json').version var APP_VERSION = require('./package.json').version
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings') var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
module.exports = { module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'), APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
@@ -19,7 +17,9 @@ module.exports = {
APP_VERSION: APP_VERSION, APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)', APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update', AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' +
'?version=' + APP_VERSION + '&platform=' + process.platform,
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report', CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
@@ -27,14 +27,9 @@ module.exports = {
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'), CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'), CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
DELAYED_INIT: 3000 /* 3 seconds */,
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop', GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master', GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
HOME_PAGE_URL: 'https://webtorrent.io',
IS_PORTABLE: isPortable(), IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(), IS_PRODUCTION: isProduction(),
@@ -58,11 +53,7 @@ function getConfigPath () {
} }
function isPortable () { function isPortable () {
try { return process.platform === 'win32' && isProduction() && pathExists(PORTABLE_PATH)
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
} catch (err) {
return false
}
} }
function isProduction () { function isProduction () {
@@ -70,7 +61,7 @@ function isProduction () {
return false return false
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
return !/\/Electron\.app\//.test(process.execPath) return !/\/Electron\.app\/Contents\/MacOS\/Electron$/.test(process.execPath)
} }
if (process.platform === 'win32') { if (process.platform === 'win32') {
return !/\\electron\.exe$/.test(process.execPath) return !/\\electron\.exe$/.test(process.execPath)

View File

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

View File

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

View File

@@ -1,38 +0,0 @@
module.exports = {
init
}
var electron = require('electron')
var get = require('simple-get')
var config = require('../config')
var log = require('./log')
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform
function init () {
get.concat(ANNOUNCEMENT_URL, function (err, res, data) {
if (err) return log('failed to retrieve remote message')
if (res.statusCode !== 200) return log('no remote message')
try {
data = JSON.parse(data.toString())
} catch (err) {
data = {
title: 'WebTorrent Desktop Announcement',
message: 'WebTorrent Desktop Announcement',
detail: data.toString()
}
}
electron.dialog.showMessageBox({
type: 'info',
buttons: ['OK'],
title: data.title,
message: data.message,
detail: data.detail
}, function () {})
})
}

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

@@ -0,0 +1,50 @@
module.exports = {
init
}
var electron = require('electron')
var get = require('simple-get')
var config = require('../config')
var log = require('./log')
var windows = require('./windows')
var autoUpdater = electron.autoUpdater
function init () {
autoUpdater.on('error', function (err) {
log.error('App update error: ' + err.message || err)
})
autoUpdater.setFeedURL(config.AUTO_UPDATE_URL)
/*
* We always check for updates on app startup. To keep app startup fast, we delay this
* first check so it happens when there is less going on.
*/
setTimeout(checkForUpdates, config.AUTO_UPDATE_CHECK_STARTUP_DELAY)
autoUpdater.on('checking-for-update', () => log('Checking for app update'))
autoUpdater.on('update-available', () => log('App update available'))
autoUpdater.on('update-not-available', () => log('App update not available'))
autoUpdater.on('update-downloaded', function (e, releaseNotes, releaseName, releaseDate, updateURL) {
log('App update downloaded: ', releaseName, updateURL)
})
}
function checkForUpdates () {
// Electron's built-in auto updater only supports Mac and Windows, for now
if (process.platform !== 'linux') {
return autoUpdater.checkForUpdates()
}
// If we're on Linux, we have to do it ourselves
get.concat(config.AUTO_UPDATE_URL, function (err, res, data) {
if (err) return log('Error checking for app update: ' + err.message)
if (![200, 204].includes(res.statusCode)) return log('Error checking for app update, got HTTP ' + res.statusCode)
if (res.statusCode !== 200) return
var obj = JSON.parse(data)
windows.main.send('dispatch', 'updateAvailable', obj.version)
})
}

View File

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

View File

@@ -1,21 +1,19 @@
console.time('init')
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
var ipcMain = electron.ipcMain var ipcMain = electron.ipcMain
var announcement = require('./announcement') var autoUpdater = require('./auto-updater')
var config = require('../config') var config = require('../config')
var crashReporter = require('../crash-reporter') var crashReporter = require('../crash-reporter')
var handlers = require('./handlers') var handlers = require('./handlers')
var ipc = require('./ipc') var ipc = require('./ipc')
var log = require('./log') var log = require('./log')
var menu = require('./menu') var menu = require('./menu')
var shortcuts = require('./shortcuts')
var squirrelWin32 = require('./squirrel-win32') var squirrelWin32 = require('./squirrel-win32')
var tray = require('./tray')
var updater = require('./updater')
var windows = require('./windows') var windows = require('./windows')
var tray = require('./tray')
var shouldQuit = false var shouldQuit = false
var argv = sliceArgv(process.argv) var argv = sliceArgv(process.argv)
@@ -43,7 +41,6 @@ function init () {
app.setPath('userData', config.CONFIG_PATH) 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.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false app.isQuitting = false
@@ -55,23 +52,21 @@ function init () {
app.on('will-finish-launching', function () { app.on('will-finish-launching', function () {
crashReporter.init() crashReporter.init()
autoUpdater.init()
}) })
app.on('ready', function () { app.on('ready', function () {
isReady = true menu.init()
windows.createMainWindow() windows.createMainWindow()
windows.createWebTorrentHiddenWindow() windows.createWebTorrentHiddenWindow()
menu.init() shortcuts.init()
tray.init()
// To keep app startup fast, some code is delayed. handlers.install()
setTimeout(delayedInit, config.DELAYED_INIT)
}) })
app.on('ipcReady', function () { app.on('ipcReady', function () {
log('Command line args:', argv) log('Command line args:', argv)
processArgv(argv) processArgv(argv)
console.timeEnd('init')
}) })
app.on('before-quit', function (e) { app.on('before-quit', function (e) {
@@ -85,17 +80,10 @@ function init () {
}) })
app.on('activate', function () { app.on('activate', function () {
if (isReady) windows.createMainWindow() windows.createMainWindow()
}) })
} }
function delayedInit () {
announcement.init()
tray.init()
handlers.install()
updater.init()
}
function onOpen (e, torrentId) { function onOpen (e, torrentId) {
e.preventDefault() e.preventDefault()
@@ -130,27 +118,18 @@ function sliceArgv (argv) {
} }
function processArgv (argv) { function processArgv (argv) {
var pathsToOpen = []
argv.forEach(function (arg) { argv.forEach(function (arg) {
if (arg === '-n') { if (arg === '-n') {
menu.showOpenSeedFiles() windows.main.send('dispatch', 'showOpenSeedFiles')
} else if (arg === '-o') { } else if (arg === '-o') {
menu.showOpenTorrentFile() windows.main.send('dispatch', 'showOpenTorrentFile')
} else if (arg === '-u') { } else if (arg === '-u') {
menu.showOpenTorrentAddress() windows.main.send('showOpenTorrentAddress')
} else if (arg.startsWith('-psn')) { } else if (arg.startsWith('-psn')) {
// Ignore OS X launchd "process serial number" argument // Ignore OS X launchd "process serial number" argument
// More: https://github.com/feross/webtorrent-desktop/issues/214 // More: https://github.com/feross/webtorrent-desktop/issues/214
} else { } else {
pathsToOpen.push(arg) windows.main.send('dispatch', 'onOpen', arg)
} }
}) })
if (pathsToOpen.length > 0) openFilePaths(pathsToOpen)
}
// Send files to the renderer process
// Opening files means either adding torrents, creating and seeding a torrent
// from files, or adding subtitles
function openFilePaths (paths) {
windows.main.send('dispatch', 'onOpen', paths)
} }

View File

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

View File

@@ -1,14 +1,11 @@
module.exports = { module.exports = {
init, init,
onPlayerClose,
onPlayerOpen,
onToggleFullScreen, onToggleFullScreen,
onWindowHide, onWindowHide,
onWindowShow, onWindowShow,
onPlayerOpen,
// TODO: move these out of menu.js -- they don't belong here onPlayerClose,
showOpenSeedFiles, showOpenSeedFiles,
showOpenTorrentAddress,
showOpenTorrentFile, showOpenTorrentFile,
toggleFullScreen toggleFullScreen
} }
@@ -21,26 +18,20 @@ var config = require('../config')
var log = require('./log') var log = require('./log')
var windows = require('./windows') var windows = require('./windows')
var appMenu var appMenu, dockMenu
function init () { function init () {
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate()) appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
electron.Menu.setApplicationMenu(appMenu) electron.Menu.setApplicationMenu(appMenu)
if (app.dock) { dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate()) if (app.dock) app.dock.setMenu(dockMenu)
app.dock.setMenu(dockMenu)
}
} }
function toggleFullScreen (flag) { function toggleFullScreen (flag) {
log('toggleFullScreen %s', flag) log('toggleFullScreen %s', flag)
if (windows.main && windows.main.isVisible()) { if (windows.main && windows.main.isVisible()) {
flag = flag != null ? flag : !windows.main.isFullScreen() flag = flag != null ? flag : !windows.main.isFullScreen()
if (flag) {
// Allows the window to use the full screen in fullscreen mode (OS X).
windows.main.setAspectRatio(0)
}
windows.main.setFullScreen(flag) windows.main.setFullScreen(flag)
} }
} }
@@ -55,25 +46,6 @@ function toggleFloatOnTop (flag) {
} }
} }
function toggleDevTools () {
log('toggleDevTools')
if (windows.main) {
windows.main.toggleDevTools()
}
}
function showWebTorrentWindow () {
log('showWebTorrentWindow')
windows.webtorrent.show()
windows.webtorrent.webContents.openDevTools({ detach: true })
}
function playPause () {
if (windows.main) {
windows.main.send('dispatch', 'playPause')
}
}
function increaseVolume () { function increaseVolume () {
if (windows.main) { if (windows.main) {
windows.main.send('dispatch', 'changeVolume', 0.1) windows.main.send('dispatch', 'changeVolume', 0.1)
@@ -86,47 +58,16 @@ function decreaseVolume () {
} }
} }
function openSubtitles () { function toggleDevTools () {
log('toggleDevTools')
if (windows.main) { if (windows.main) {
windows.main.send('dispatch', 'openSubtitles') windows.main.toggleDevTools()
} }
} }
function skipForward () { function showWebTorrentWindow () {
if (windows.main) { windows.webtorrent.show()
windows.main.send('dispatch', 'skip', 1) windows.webtorrent.webContents.openDevTools({ detach: true })
}
}
function skipBack () {
if (windows.main) {
windows.main.send('dispatch', 'skip', -1)
}
}
function increasePlaybackRate () {
if (windows.main) {
windows.main.send('dispatch', 'changePlaybackRate', 1)
}
}
function decreasePlaybackRate () {
if (windows.main) {
windows.main.send('dispatch', 'changePlaybackRate', -1)
}
}
// Open the preferences window
function showPreferences () {
if (windows.main) {
windows.main.send('dispatch', 'preferences')
}
}
function escapeBack () {
if (windows.main) {
windows.main.send('dispatch', 'escapeBack')
}
} }
function onWindowShow () { function onWindowShow () {
@@ -142,27 +83,13 @@ function onWindowHide () {
} }
function onPlayerOpen () { function onPlayerOpen () {
log('onPlayerOpen')
getMenuItem('Play/Pause').enabled = true
getMenuItem('Increase Volume').enabled = true getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true getMenuItem('Decrease Volume').enabled = true
getMenuItem('Add Subtitles File...').enabled = true
getMenuItem('Step Forward').enabled = true
getMenuItem('Step Backward').enabled = true
getMenuItem('Increase Speed').enabled = true
getMenuItem('Decrease Speed').enabled = true
} }
function onPlayerClose () { function onPlayerClose () {
log('onPlayerClose')
getMenuItem('Play/Pause').enabled = false
getMenuItem('Increase Volume').enabled = false getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false getMenuItem('Decrease Volume').enabled = false
getMenuItem('Add Subtitles File...').enabled = false
getMenuItem('Step Forward').enabled = false
getMenuItem('Step Backward').enabled = false
getMenuItem('Increase Speed').enabled = false
getMenuItem('Decrease Speed').enabled = false
} }
function onToggleFullScreen (isFullScreen) { function onToggleFullScreen (isFullScreen) {
@@ -181,27 +108,17 @@ function getMenuItem (label) {
} }
} }
// Prompts the user for a file, then creates a torrent. Only allows a single file // Prompts the user for a file or folder, then makes a torrent out of the data
// selection.
function showOpenSeedFile () {
electron.dialog.showOpenDialog({
title: 'Select a file for the torrent file.',
properties: [ 'openFile' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
windows.main.send('dispatch', 'showCreateTorrent', selectedPaths)
})
}
// Prompts the user for a file or directory, then creates a torrent. Only allows a single
// selection. To create a multi-file torrent, the user must select a directory.
function showOpenSeedFiles () { function showOpenSeedFiles () {
// Allow only a single selection
// To create a multi-file torrent, the user must select a folder
electron.dialog.showOpenDialog({ electron.dialog.showOpenDialog({
title: 'Select a file or folder for the torrent file.', title: 'Select a file or folder for the torrent file.',
properties: [ 'openFile', 'openDirectory' ] properties: [ 'openFile', 'openDirectory' ]
}, function (selectedPaths) { }, function (filenames) {
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(filenames)) return
windows.main.send('dispatch', 'showCreateTorrent', selectedPaths) var fileOrFolder = filenames[0]
windows.main.send('dispatch', 'showCreateTorrent', fileOrFolder)
}) })
} }
@@ -211,10 +128,10 @@ function showOpenTorrentFile () {
title: 'Select a .torrent file to open.', title: 'Select a .torrent file to open.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ] properties: [ 'openFile', 'multiSelections' ]
}, function (selectedPaths) { }, function (filenames) {
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(filenames)) return
selectedPaths.forEach(function (selectedPath) { filenames.forEach(function (filename) {
windows.main.send('dispatch', 'addTorrent', selectedPath) windows.main.send('dispatch', 'addTorrent', filename)
}) })
}) })
} }
@@ -225,38 +142,44 @@ function showOpenTorrentAddress () {
} }
function getAppMenuTemplate () { function getAppMenuTemplate () {
var fileMenu = [
{
label: 'Create New Torrent...',
accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
},
{
type: 'separator'
},
{
label: process.platform === 'windows' ? 'Close' : 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
// File > Quit for Linux users with distros where the system tray is broken
if (process.platform === 'linux') {
fileMenu.push({
label: 'Quit',
click: () => app.quit()
})
}
var template = [ var template = [
{ {
label: 'File', label: 'File',
submenu: [ submenu: fileMenu
{
label: process.platform === 'darwin'
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
},
{
type: 'separator'
},
{
label: process.platform === 'win32'
? 'Close'
: 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
}, },
{ {
label: 'Edit', label: 'Edit',
@@ -280,14 +203,6 @@ function getAppMenuTemplate () {
label: 'Select All', label: 'Select All',
accelerator: 'CmdOrCtrl+A', accelerator: 'CmdOrCtrl+A',
role: 'selectall' role: 'selectall'
},
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => showPreferences()
} }
] ]
}, },
@@ -310,6 +225,21 @@ function getAppMenuTemplate () {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
enabled: false
},
{
type: 'separator'
},
{ {
label: 'Developer', label: 'Developer',
submenu: [ submenu: [
@@ -328,78 +258,17 @@ function getAppMenuTemplate () {
click: showWebTorrentWindow click: showWebTorrentWindow
} }
] ]
},
{
type: 'separator'
},
{
label: 'Go Back',
accelerator: 'Esc',
click: escapeBack
} }
] ]
}, },
{ {
label: 'Playback', label: 'Window',
role: 'window',
submenu: [ submenu: [
{ {
label: 'Play/Pause', label: 'Minimize',
accelerator: 'Space', accelerator: 'CmdOrCtrl+M',
click: playPause, role: 'minimize'
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
enabled: false
},
{
type: 'separator'
},
{
label: 'Step Forward',
accelerator: 'CmdOrCtrl+Alt+Right',
click: skipForward,
enabled: false
},
{
label: 'Step Backward',
accelerator: 'CmdOrCtrl+Alt+Left',
click: skipBack,
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Speed',
accelerator: 'CmdOrCtrl+=',
click: increasePlaybackRate,
enabled: false
},
{
label: 'Decrease Speed',
accelerator: 'CmdOrCtrl+-',
click: decreasePlaybackRate,
enabled: false
},
{
type: 'separator'
},
{
label: 'Add Subtitles File...',
click: openSubtitles,
enabled: false
} }
] ]
}, },
@@ -409,7 +278,7 @@ function getAppMenuTemplate () {
submenu: [ submenu: [
{ {
label: 'Learn more about ' + config.APP_NAME, label: 'Learn more about ' + config.APP_NAME,
click: () => electron.shell.openExternal(config.HOME_PAGE_URL) click: () => electron.shell.openExternal('https://webtorrent.io')
}, },
{ {
label: 'Contribute on GitHub', label: 'Contribute on GitHub',
@@ -420,14 +289,14 @@ function getAppMenuTemplate () {
}, },
{ {
label: 'Report an Issue...', label: 'Report an Issue...',
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES) click: () => electron.shell.openExternal(config.GITHUB_URL + '/issues')
} }
] ]
} }
] ]
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// Add WebTorrent app menu (OS X) // WebTorrent menu (OS X)
template.unshift({ template.unshift({
label: config.APP_NAME, label: config.APP_NAME,
submenu: [ submenu: [
@@ -438,14 +307,6 @@ function getAppMenuTemplate () {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => showPreferences()
},
{
type: 'separator'
},
{ {
label: 'Services', label: 'Services',
role: 'services', role: 'services',
@@ -479,35 +340,17 @@ function getAppMenuTemplate () {
] ]
}) })
// Add Window menu (OS X) // Window menu (OS X)
template.splice(5, 0, { template[4].submenu.push(
label: 'Window', {
role: 'window', type: 'separator'
submenu: [ },
{ {
label: 'Minimize', label: 'Bring All to Front',
accelerator: 'CmdOrCtrl+M', role: 'front'
role: 'minimize' }
}, )
{ } else {
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
]
})
}
// In Linux and Windows it is not possible to open both folders and files
if (process.platform === 'linux' || process.platform === 'win32') {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: showOpenSeedFile
})
// Help menu (Windows, Linux) // Help menu (Windows, Linux)
template[4].submenu.push( template[4].submenu.push(
{ {
@@ -519,15 +362,6 @@ function getAppMenuTemplate () {
} }
) )
} }
// Add "File > Quit" menu item so Linux distros where the system tray icon is missing
// will have a way to quit the app.
if (process.platform === 'linux') {
// File menu (Linux)
template[0].submenu.push({
label: 'Quit',
click: () => app.quit()
})
}
return template return template
} }

View File

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

View File

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

View File

@@ -1,76 +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')
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform
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(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(AUTO_UPDATE_URL)
electron.autoUpdater.checkForUpdates()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,22 +50,19 @@ table {
} }
@keyframes fadein { @keyframes fadein {
from { from { opacity: 0; }
opacity: 0; to { opacity: 1; }
}
to {
opacity: 1;
}
} }
.app { .app {
-webkit-user-select: none; -webkit-user-select: none;
-webkit-app-region: drag; -webkit-app-region: drag;
height: 100%; height: 100%;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
animation: fadein 0.3s;
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
animation: fadein 1s;
} }
.app:not(.is-focused) { .app:not(.is-focused) {
@@ -97,20 +94,11 @@ table {
word-wrap: normal; word-wrap: normal;
white-space: nowrap; white-space: nowrap;
direction: ltr; direction: ltr;
opacity: 0.85;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
.icon.disabled {
opacity: 0.3;
}
.icon:not(.disabled):hover {
opacity: 1;
}
/* /*
* UTILITY CLASSES * UTILITY CLASSES
*/ */
@@ -121,14 +109,18 @@ table {
white-space: nowrap; white-space: nowrap;
} }
.float-left { .disabled {
float: left; opacity: 0.3;
} }
.float-right { .float-right {
float: right; float: right;
} }
.expand-collapse {
cursor: pointer;
}
.expand-collapse.expanded::before { .expand-collapse.expanded::before {
content: '▲' content: '▲'
} }
@@ -156,8 +148,8 @@ table {
.header { .header {
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
border-bottom: 1px solid rgb(20, 20, 20); border-bottom: 1px solid rgb(20, 20, 20);
height: 38px; /* vertically center OS menu buttons (OS X) */ height: 37px; /* vertically center OS menu buttons (OS X) */
padding-top: 7px; padding-top: 6px;
overflow: hidden; overflow: hidden;
flex: 0 1 auto; flex: 0 1 auto;
opacity: 1; opacity: 1;
@@ -176,13 +168,7 @@ table {
} }
.app.view-player .header { .app.view-player .header {
background: rgba(40, 40, 40, 0.8); opacity: 0.8;
border-bottom: none;
}
.app.view-player.is-win32 .header,
.app.view-player.is-linux .header {
background: none;
} }
.app.hide-video-controls.view-player .header { .app.hide-video-controls.view-player .header {
@@ -190,8 +176,12 @@ table {
cursor: none; cursor: none;
} }
.app.hide-header .header {
display: none;
}
.header .title { .header .title {
opacity: 0.7; opacity: 0.6;
position: absolute; position: absolute;
margin-top: 1px; margin-top: 1px;
padding: 0 150px 0 150px; padding: 0 150px 0 150px;
@@ -202,22 +192,35 @@ table {
.header .nav { .header .nav {
font-weight: bold; font-weight: bold;
margin-right: 9px;
} }
.header .nav.left { .header .nav.left {
margin-left: 10px; float: left;
}
.header .nav.right {
margin-right: 10px;
} }
.app.is-darwin:not(.is-fullscreen) .header .nav.left { .app.is-darwin:not(.is-fullscreen) .header .nav.left {
margin-left: 78px; margin-left: 78px;
} }
.header .back, .header .nav.right {
.header .forward { float: right;
}
.header .nav * {
opacity: 0.6;
}
.header .nav .disabled {
opacity: 0.1;
}
.header .nav *:not(.disabled):hover {
opacity: 1;
}
.header .nav .back,
.header .nav .forward {
font-size: 30px; font-size: 30px;
margin-top: -3px; margin-top: -3px;
} }
@@ -273,6 +276,7 @@ table {
} }
.modal label { .modal label {
font-size: 16px;
font-weight: bold; font-weight: bold;
} }
@@ -363,6 +367,7 @@ button { /* Rectangular text buttons */
border-radius: 3px; border-radius: 3px;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
cursor: pointer;
color: #aaa; color: #aaa;
outline: none; outline: none;
} }
@@ -429,7 +434,7 @@ input {
.torrent, .torrent,
.torrent-placeholder { .torrent-placeholder {
height: 100px; height: 120px;
} }
.torrent:not(:last-child) { .torrent:not(:last-child) {
@@ -442,9 +447,9 @@ input {
.torrent .metadata { .torrent .metadata {
position: absolute; position: absolute;
top: 25px; top: 20px;
left: 15px; left: 20px;
right: 15px; right: 20px;
width: calc(100% - 40px); width: calc(100% - 40px);
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px; text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
} }
@@ -454,15 +459,12 @@ input {
} }
.torrent .metadata span:not(:last-child)::after { .torrent .metadata span:not(:last-child)::after {
content: ' '; content: ' ';
opacity: 0.7;
padding-left: 4px;
padding-right: 4px;
} }
.torrent .buttons { .torrent .buttons {
position: absolute; position: absolute;
top: 29px; top: 25px;
right: 10px; right: 10px;
align-items: center; align-items: center;
display: none; display: none;
@@ -536,11 +538,6 @@ input {
} }
} }
.torrent .buttons .play.resume-position {
position: relative;
-webkit-clip-path: circle(18px at center);
}
.torrent .buttons .delete { .torrent .buttons .delete {
opacity: 0.5; opacity: 0.5;
} }
@@ -549,13 +546,15 @@ input {
opacity: 0.7; opacity: 0.7;
} }
.torrent .buttons .radial-progress { .torrent .name {
position: absolute; font-size: 1.5em;
font-weight: bold;
line-height: 1.5em;
} }
.torrent .name { .torrent .status,
font-size: 18px; .torrent .status2 {
font-weight: bold; font-size: 1em;
line-height: 1.5em; line-height: 1.5em;
} }
@@ -602,7 +601,11 @@ body.drag .app::after {
} }
.torrent-details { .torrent-details {
padding: 8em 0 20px 0; padding: 8em 20px 20px 20px;
}
.torrent-details .open-folder {
float: right;
} }
.torrent-details table { .torrent-details table {
@@ -616,35 +619,25 @@ body.drag .app::after {
height: 28px; height: 28px;
} }
.torrent-details td { .torrent-details tr:hover,
vertical-align: center; .torrent-details .open-folder:hover {
}
.torrent-details tr:hover {
background-color: rgba(200, 200, 200, 0.3); background-color: rgba(200, 200, 200, 0.3);
} }
.torrent-details td { .torrent-details td {
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
vertical-align: middle; vertical-align: bottom;
}
.torrent-details td .icon {
font-size: 18px;
position: relative;
top: 3px;
} }
.torrent-details td.col-icon { .torrent-details td.col-icon {
width: 3em; width: 2em;
padding-left: 16px;
} }
.torrent-details td.col-icon .radial-progress { .torrent-details td.col-icon .icon {
position: absolute; font-size: 18px;
margin-top: 4px; position: relative;
margin-left: 0.5px; top: 3px;
} }
.torrent-details td.col-name { .torrent-details td.col-name {
@@ -662,12 +655,6 @@ body.drag .app::after {
text-align: right; text-align: right;
} }
.torrent-details td.col-select {
width: 3em;
padding-right: 13px;
text-align: right;
}
/* /*
* PLAYER * PLAYER
*/ */
@@ -687,16 +674,18 @@ body.drag .app::after {
background-position: center; background-position: center;
} }
.player video { .player .video-player {
display: block; display: block;
width: 100%; width: 100%;
height: 100%;
position: relative;
} }
/* /*
* PLAYER CONTROLS * PLAYER CONTROLS
*/ */
.player .controls { .player-controls {
position: fixed; position: fixed;
background: rgba(40, 40, 40, 0.8); background: rgba(40, 40, 40, 0.8);
width: 100%; width: 100%;
@@ -705,63 +694,7 @@ body.drag .app::after {
transition: opacity 0.15s ease-out; transition: opacity 0.15s ease-out;
} }
.player .controls .icon { .app.hide-video-controls .player-controls {
display: block;
margin: 8px;
font-size: 22px;
opacity: 0.85;
/*
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
max-width: 23px;
overflow: hidden;
}
.player .controls .icon:hover {
opacity: 1;
}
.player .controls .play-pause {
font-size: 28px;
margin-top: 5px;
margin-right: 10px;
margin-left: 15px;
}
.player .controls .volume-slider {
-webkit-appearance: none;
-webkit-app-region: no-drag;
width: 60px;
height: 3px;
margin: 18px 8px 8px 0;
border: none;
padding: 0;
opacity: 0.85;
vertical-align: sub;
}
.player .controls .time,
.player .controls .rate {
font-weight: 100;
font-size: 13px;
margin: 9px 8px 8px 8px;
opacity: 0.8;
}
.player .controls .icon.closed-captions {
font-size: 26px;
margin-top: 6px;
}
.player .controls .icon.fullscreen {
font-size: 26px;
margin-right: 15px;
margin-top: 6px;
}
.app.hide-video-controls .player .controls {
opacity: 0; opacity: 0;
} }
@@ -769,16 +702,13 @@ body.drag .app::after {
cursor: none; cursor: none;
} }
/* TODO: find better way to handle this (that also .app.hide-video-controls .player .player-controls:hover {
* keeps the header visible too).
*/
.app.hide-video-controls .player .controls:hover {
opacity: 1; opacity: 1;
cursor: default; cursor: default;
} }
/* invisible click target for scrubbing */ /* invisible click target for scrubbing */
.player .controls .scrub-bar { .player-controls .scrub-bar {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 23px; /* 3px .loading-bar plus 10px above and below */ height: 23px; /* 3px .loading-bar plus 10px above and below */
@@ -787,7 +717,7 @@ body.drag .app::after {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
.player .controls .loading-bar { .player-controls .loading-bar {
position: relative; position: relative;
width: 100%; width: 100%;
top: -3px; top: -3px;
@@ -797,14 +727,14 @@ body.drag .app::after {
position: absolute; position: absolute;
} }
.player .controls .loading-bar-part { .player-controls .loading-bar-part {
position: absolute; position: absolute;
background-color: #dd0000; background-color: #dd0000;
top: 0; top: 0;
height: 100%; height: 100%;
} }
.player .controls .playback-cursor { .player-controls .playback-cursor {
position: absolute; position: absolute;
top: -3px; top: -3px;
background-color: #FFF; background-color: #FFF;
@@ -813,26 +743,86 @@ body.drag .app::after {
border-radius: 50%; border-radius: 50%;
margin-top: 0; margin-top: 0;
margin-left: 0; margin-left: 0;
transition-property: width, height, top, margin-left; transition-property: width, height, border-radius, margin-top, margin-left;
transition-duration: 0.1s; transition-duration: 0.1s;
transition-timing-function: ease-out; transition-timing-function: ease-out;
} }
.player .controls .closed-captions.active, .player-controls .play-pause {
.player .controls .device.active { display: block;
width: 30px;
height: 30px;
padding: 5px;
margin: 0 auto;
}
.player-controls .device,
.player-controls .fullscreen,
.player-controls .closed-captions,
.player-controls .volume-icon,
.player-controls .back {
display: block;
width: 20px;
height: 20px;
margin: 5px;
}
.player-controls .volume,
.player-controls .back {
float: left;
}
.player-controls .device,
.player-controls .closed-captions,
.player-controls .fullscreen {
float: right;
}
.player-controls .fullscreen {
margin-right: 15px;
}
.player-controls .volume-icon,
.player-controls .device {
font-size: 18px; /* make the cast icons less huge */
margin-top: 8px !important;
}
.player-controls .closed-captions.active,
.player-controls .device.active {
color: #9af; color: #9af;
} }
.player .controls .volume-slider::-webkit-slider-thumb { .player-controls .volume {
display: block;
width: 90px;
}
.player-controls .volume-icon {
float: left;
margin-right: 0px;
}
.player-controls .volume-slider {
-webkit-appearance: none;
width: 50px;
height: 3px;
border: none;
padding: 0;
vertical-align: sub;
}
.player-controls .volume-slider::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
-webkit-app-region: no-drag;
background-color: #fff; background-color: #fff;
width: 13px; opacity: 1.0;
height: 13px; width: 10px;
height: 10px;
border: 1px solid #303233;
border-radius: 50%; border-radius: 50%;
} }
.player .controls .volume-slider:focus { .player-controls .volume-slider:focus {
outline: none; outline: none;
} }
@@ -842,27 +832,19 @@ body.drag .app::after {
.player .playback-bar:hover .playback-cursor { .player .playback-bar:hover .playback-cursor {
top: -8px; top: -8px;
margin-left: -5px;
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
/**
* Set the cue text position so it appears above the player controls.
*/
video::-webkit-media-text-track-container {
bottom: 60px;
transition: bottom 0.1s ease-out;
}
.app.hide-video-controls video::-webkit-media-text-track-container {
bottom: 30px;
}
::cue { ::cue {
background: none; background: none;
color: #FFF; color: #FFF;
font: 24px;
line-height: 1.3em;
text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0; text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0;
} }
/* /*
* CHROMECAST / AIRPLAY CONTROLS * CHROMECAST / AIRPLAY CONTROLS
*/ */
@@ -912,173 +894,6 @@ video::-webkit-media-text-track-container {
margin-right: 4px !important; margin-right: 4px !important;
} }
/*
* Preferences page, based on Atom settings style
*/
.preferences {
font-size: 12px;
line-height: calc(10/7);
}
.preferences .text {
color: #a8a8a8;
}
.preferences .icon {
color: rgba(170, 170, 170, 0.6);
font-size: 16px;
margin-right: 0.2em;
}
.preferences .btn {
display: inline-block;
-webkit-appearance: button;
margin: 0;
font-weight: normal;
text-align: center;
vertical-align: middle;
border-color: #cccccc;
border-radius: 3px;
color: #9da5b4;
text-shadow: none;
border: 1px solid #181a1f;
background-color: #3d3d3d;
white-space: initial;
font-size: 0.889em;
line-height: 1;
padding: 0.5em 0.75em;
}
.preferences .btn .icon {
margin: 0;
color: #a8a8a8;
}
.preferences .help .icon {
vertical-align: sub;
}
.preferences .preferences-panel .control-group + .control-group {
margin-top: 1.5em;
}
.preferences .section {
padding: 20px;
border-top: 1px solid #181a1f;
}
.preferences .section:first {
border-top: 0px;
}
.preferences .section:first-child,
.preferences .section:last-child {
padding: 20px;
}
.preferences .section.section:empty {
padding: 0;
border-top: none;
}
.preferences .section-container {
width: 100%;
max-width: 800px;
}
.preferences section .section-heading,
.preferences .section .section-heading {
margin-bottom: 10px;
color: #dcdcdc;
font-size: 1.75em;
font-weight: bold;
line-height: 1;
-webkit-user-select: none;
cursor: default;
}
.preferences .sub-section-heading.icon:before,
.preferences .section-heading.icon:before {
margin-right: 8px;
}
.preferences .section-heading-count {
margin-left: .5em;
}
.preferences .section-body {
margin-top: 20px;
}
.preferences .sub-section {
margin: 20px 0;
}
.preferences .sub-section .sub-section-heading {
color: #dcdcdc;
font-size: 1.4em;
font-weight: bold;
line-height: 1;
-webkit-user-select: none;
}
.preferences .preferences-panel label {
color: #a8a8a8;
}
.preferences .preferences-panel .control-group + .control-group {
margin-top: 1.5em;
}
.preferences .preferences-panel .control-group .editor-container {
margin: 0;
}
.preferences .preference-title {
font-size: 1.2em;
-webkit-user-select: none;
display: inline-block;
}
.preferences .preference-description {
color: rgba(170, 170, 170, 0.6);
-webkit-user-select: none;
cursor: default;
}
.preferences input {
font-size: 1.1em;
line-height: 1.15em;
max-height: none;
width: 100%;
padding-left: 0.5em;
border-radius: 3px;
color: #a8a8a8;
border: 1px solid #181a1f;
background-color: #1b1d23;
}
.preferences input::-webkit-input-placeholder {
color: rgba(170, 170, 170, 0.6);
}
.preferences .control-group input {
margin-top: 0.2em;
}
.preferences .control-group input.file-picker-text {
width: calc(100% - 40px);
}
.preferences .control-group .checkbox .icon {
font-size: 1.5em;
margin: 0;
vertical-align: text-bottom;
}
/* /*
* MEDIA OVERLAY / AUDIO DETAILS * MEDIA OVERLAY / AUDIO DETAILS
*/ */
@@ -1147,6 +962,10 @@ video::-webkit-media-text-track-container {
z-index: 1; z-index: 1;
} }
.app.hide-header .error-popover {
top: 0px;
}
.error-popover.hidden { .error-popover.hidden {
display: none; display: none;
} }
@@ -1170,70 +989,3 @@ video::-webkit-media-text-track-container {
.error-popover .error:last-child { .error-popover .error:last-child {
border-bottom: none; border-bottom: none;
} }
.error-text {
color: #c44;
}
/*
* RADIAL PROGRESS BAR
*/
.radial-progress {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #888;
}
.radial-progress .circle .mask,
.radial-progress .circle .fill {
width: 16px;
height: 16px;
position: absolute;
border-radius: 50%;
-webkit-backface-visibility: hidden;
}
.radial-progress .circle .mask {
clip: rect(0px, 16px, 16px, 8px);
}
.radial-progress .circle .fill {
clip: rect(0px, 8px, 16px, 0px);
background-color: white;
}
.radial-progress .inset {
position: absolute;
width: 12px;
height: 12px;
margin: 2px 0 0 2px;
border-radius: 50%;
background-color: black;
}
.radial-progress-large {
width: 40px;
height: 40px;
}
.radial-progress-large .circle .mask,
.radial-progress-large .circle .fill {
width: 40px;
height: 40px;
}
.radial-progress-large .circle .mask {
clip: rect(0px, 40px, 40px, 20px);
}
.radial-progress-large .circle .fill {
clip: rect(0px, 20px, 40px, 0px);
background-color: white;
}
.radial-progress-large .inset {
width: 32px;
height: 32px;
margin: 4px 0 0 4px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,7 @@ module.exports = {
play, play,
pause, pause,
seek, seek,
setVolume, setVolume
setRate
} }
var airplay = require('airplay-js') var airplay = require('airplay-js')
@@ -345,22 +344,6 @@ function pause () {
} }
} }
function setRate (rate) {
var device
var result = true
if (state.playing.location === 'chromecast') {
// TODO find how to control playback rate on chromecast
castCallback()
result = false
} else if (state.playing.location === 'airplay') {
device = state.devices.airplay
device.rate(rate, castCallback)
} else {
result = false
}
return result
}
function seek (time) { function seek (time) {
var device = getDevice() var device = getDevice()
if (device) { if (device) {

View File

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

View File

@@ -4,123 +4,81 @@ function LocationHistory () {
if (!new.target) return new LocationHistory() if (!new.target) return new LocationHistory()
this._history = [] this._history = []
this._forward = [] this._forward = []
this._pending = false this._pending = null
} }
LocationHistory.prototype.url = function () { LocationHistory.prototype.go = function (page, cb) {
return this.current() && this.current().url console.log('go', page)
this.clearForward()
this._go(page, cb)
}
LocationHistory.prototype._go = function (page, cb) {
if (this._pending) return
if (page.onbeforeload) {
this._pending = page
page.onbeforeload((err) => {
if (this._pending !== page) return /* navigation was cancelled */
this._pending = null
if (err) {
if (cb) cb(err)
return
}
this._history.push(page)
if (cb) cb()
})
} else {
this._history.push(page)
if (cb) cb()
}
}
LocationHistory.prototype.back = function (cb) {
if (this._history.length <= 1) return
var page = this._history.pop()
if (page.onbeforeunload) {
// TODO: this is buggy. If the user clicks back twice, then those pages
// may end up in _forward in the wrong order depending on which onbeforeunload
// call finishes first.
page.onbeforeunload(() => {
this._forward.push(page)
if (cb) cb()
})
} else {
this._forward.push(page)
if (cb) cb()
}
}
LocationHistory.prototype.forward = function (cb) {
if (this._forward.length === 0) return
var page = this._forward.pop()
this._go(page, cb)
}
LocationHistory.prototype.clearForward = function () {
this._forward = []
} }
LocationHistory.prototype.current = function () { LocationHistory.prototype.current = function () {
return this._history[this._history.length - 1] return this._history[this._history.length - 1]
} }
LocationHistory.prototype.go = function (page, cb) {
if (!cb) cb = noop
if (this._pending) return cb(null)
console.log('go', page)
this.clearForward()
this._go(page, cb)
}
LocationHistory.prototype.back = function (cb) {
var self = this
if (!cb) cb = noop
if (self._history.length <= 1 || self._pending) return cb(null)
var page = self._history.pop()
self._unload(page, done)
function done (err) {
if (err) return cb(err)
self._forward.push(page)
self._load(self.current(), cb)
}
}
LocationHistory.prototype.hasBack = function () { LocationHistory.prototype.hasBack = function () {
return this._history.length > 1 return this._history.length > 1
} }
LocationHistory.prototype.forward = function (cb) {
if (!cb) cb = noop
if (this._forward.length === 0 || this._pending) return cb(null)
var page = this._forward.pop()
this._go(page, cb)
}
LocationHistory.prototype.hasForward = function () { LocationHistory.prototype.hasForward = function () {
return this._forward.length > 0 return this._forward.length > 0
} }
LocationHistory.prototype.clearForward = function (url) { LocationHistory.prototype.pending = function () {
if (url == null) { return this._pending
this._forward = []
} else {
console.log(this._forward)
console.log(url)
this._forward = this._forward.filter(function (page) {
return page.url !== url
})
}
} }
LocationHistory.prototype.backToFirst = function (cb) { LocationHistory.prototype.clearPending = function () {
var self = this this._pending = null
if (!cb) cb = noop
if (self._history.length <= 1) return cb(null)
self.back(function (err) {
if (err) return cb(err)
self.backToFirst(cb)
})
} }
LocationHistory.prototype._go = function (page, cb) {
var self = this
if (!cb) cb = noop
self._unload(self.current(), done1)
function done1 (err) {
if (err) return cb(err)
self._load(page, done2)
}
function done2 (err) {
if (err) return cb(err)
self._history.push(page)
cb(null)
}
}
LocationHistory.prototype._load = function (page, cb) {
var self = this
self._pending = true
if (page && page.onbeforeload) page.onbeforeload(done)
else done(null)
function done (err) {
self._pending = false
cb(err)
}
}
LocationHistory.prototype._unload = function (page, cb) {
var self = this
self._pending = true
if (page && page.onbeforeunload) page.onbeforeunload(done)
else done(null)
function done (err) {
self._pending = false
cb(err)
}
}
function noop () {}

View File

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

View File

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

View File

@@ -4,18 +4,12 @@ var captureVideoFrame = require('./capture-video-frame')
var path = require('path') var path = require('path')
function torrentPoster (torrent, cb) { function torrentPoster (torrent, cb) {
// First, try to use a poster image if available // First, try to use the largest video file
var posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
// Second, try to use the largest video file
// Filter out file formats that the <video> tag definitely can't play // Filter out file formats that the <video> tag definitely can't play
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv']) var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb) if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
// Third, try to use the largest image file // Second, try to use the largest image file
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png']) var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb) if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)

View File

@@ -57,13 +57,7 @@ function getInitialState () {
* *
* Also accessible via `require('application-config')('WebTorrent').filePath` * Also accessible via `require('application-config')('WebTorrent').filePath`
*/ */
saved: {}, saved: {}
/*
* Getters, for convenience
*/
getPlayingTorrentSummary,
getPlayingFileSummary
} }
} }
@@ -80,13 +74,10 @@ function getDefaultPlayState () {
isStalled: false, isStalled: false,
lastTimeUpdate: 0, /* Unix time in ms */ lastTimeUpdate: 0, /* Unix time in ms */
mouseStationarySince: 0, /* Unix time in ms */ mouseStationarySince: 0, /* Unix time in ms */
playbackRate: 1,
subtitles: { subtitles: {
tracks: [], /* subtitle tracks, each {label, language, ...} */ tracks: [], /* subtitles file (Buffer) */
selectedIndex: -1, /* current subtitle track */ enabled: false
showMenu: false /* popover menu, above the video */ }
},
aspectRatio: 0 /* aspect ratio of the video */
} }
} }
@@ -266,21 +257,8 @@ function getDefaultSavedState () {
] ]
} }
], ],
prefs: { downloadPath: config.IS_PORTABLE
downloadPath: config.IS_PORTABLE ? path.join(config.CONFIG_PATH, 'Downloads')
? path.join(config.CONFIG_PATH, 'Downloads') : remote.app.getPath('downloads')
: remote.app.getPath('downloads')
}
} }
} }
function getPlayingTorrentSummary () {
var infoHash = this.playing.infoHash
return this.saved.torrents.find((x) => x.infoHash === infoHash)
}
function getPlayingFileSummary () {
var torrentSummary = this.getPlayingTorrentSummary()
if (!torrentSummary) return null
return torrentSummary.files[this.playing.fileIndex]
}

View File

@@ -8,13 +8,11 @@ var Header = require('./header')
var Views = { var Views = {
'home': require('./torrent-list'), 'home': require('./torrent-list'),
'player': require('./player'), 'player': require('./player'),
'create-torrent': require('./create-torrent-page'), 'create-torrent': require('./create-torrent-page')
'preferences': require('./preferences')
} }
var Modals = { var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal'), 'open-torrent-address-modal': require('./open-torrent-address-modal'),
'update-available-modal': require('./update-available-modal'), 'update-available-modal': require('./update-available-modal')
'unsupported-media-modal': require('./unsupported-media-modal')
} }
function App (state) { function App (state) {
@@ -23,20 +21,24 @@ function App (state) {
// * The mouse is over the controls or we're scrubbing (see CSS) // * The mouse is over the controls or we're scrubbing (see CSS)
// * The video is paused // * The video is paused
// * The video is playing remotely on Chromecast or Airplay // * The video is playing remotely on Chromecast or Airplay
var hideControls = state.location.url() === 'player' && var hideControls = state.location.current().url === 'player' &&
state.playing.mouseStationarySince !== 0 && state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 && new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused && !state.playing.isPaused &&
state.playing.location === 'local' && state.playing.location === 'local'
state.playing.playbackRate === 1
// Hide the header on Windows/Linux when in the player
// On OSX, the header appears as part of the title bar
var hideHeader = process.platform !== 'darwin' && state.location.current().url === 'player'
var cls = [ var cls = [
'view-' + state.location.url(), /* e.g. view-home, view-player */ 'view-' + state.location.current().url, /* e.g. view-home, view-player */
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */ 'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
] ]
if (state.window.isFullScreen) cls.push('is-fullscreen') if (state.window.isFullScreen) cls.push('is-fullscreen')
if (state.window.isFocused) cls.push('is-focused') if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls') if (hideControls) cls.push('hide-video-controls')
if (hideHeader) cls.push('hide-header')
return hx` return hx`
<div class='app ${cls.join(' ')}'> <div class='app ${cls.join(' ')}'>
@@ -51,13 +53,12 @@ function App (state) {
function getErrorPopover (state) { function getErrorPopover (state) {
var now = new Date().getTime() var now = new Date().getTime()
var recentErrors = state.errors.filter((x) => now - x.time < 5000) var recentErrors = state.errors.filter((x) => now - x.time < 5000)
var hasErrors = recentErrors.length > 0
var errorElems = recentErrors.map(function (error) { var errorElems = recentErrors.map(function (error) {
return hx`<div class='error'>${error.message}</div>` return hx`<div class='error'>${error.message}</div>`
}) })
return hx` return hx`
<div class='error-popover ${hasErrors ? 'visible' : 'hidden'}'> <div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
<div class='title'>Error</div> <div class='title'>Error</div>
${errorElems} ${errorElems}
</div> </div>
@@ -78,6 +79,6 @@ function getModal (state) {
} }
function getView (state) { function getView (state) {
var url = state.location.url() var url = state.location.current().url
return Views[url](state) return Views[url](state)
} }

View File

@@ -8,7 +8,7 @@ var createTorrent = require('create-torrent')
var path = require('path') var path = require('path')
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var {dispatch, dispatcher} = require('../lib/dispatcher') var {dispatch} = require('../lib/dispatcher')
function CreateTorrentPage (state) { function CreateTorrentPage (state) {
var info = state.location.current() var info = state.location.current()
@@ -17,19 +17,23 @@ function CreateTorrentPage (state) {
var files = info.files var files = info.files
.filter((f) => !f.name.startsWith('.')) .filter((f) => !f.name.startsWith('.'))
.map((f) => ({name: f.name, path: f.path, size: f.size})) .map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return CreateTorrentErrorPage()
// First, extract the base folder that the files are all in // First, extract the base folder that the files are all in
var pathPrefix = info.folderPath var pathPrefix = info.folderPath
if (!pathPrefix) { if (!pathPrefix) {
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix) if (files.length > 0) {
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) { pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
pathPrefix = path.dirname(pathPrefix) if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
pathPrefix = path.dirname(pathPrefix)
}
} else {
pathPrefix = files[0]
} }
} }
// Sanity check: show the number of files and total size // Sanity check: show the number of files and total size
var numFiles = files.length var numFiles = files.length
console.log('FILES', files)
var totalBytes = files var totalBytes = files
.map((f) => f.size) .map((f) => f.size)
.reduce((a, b) => a + b, 0) .reduce((a, b) => a + b, 0)
@@ -37,16 +41,8 @@ function CreateTorrentPage (state) {
// Then, use the name of the base folder (or sole file, for a single file torrent) // Then, use the name of the base folder (or sole file, for a single file torrent)
// as the default name. Show all files relative to the base folder. // as the default name. Show all files relative to the base folder.
var defaultName, basePath var defaultName = path.basename(pathPrefix)
if (files.length === 1) { var basePath = path.dirname(pathPrefix)
// Single file torrent: /a/b/foo.jpg -> torrent name "foo.jpg", path "/a/b"
defaultName = files[0].name
basePath = pathPrefix
} else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a"
defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
var maxFileElems = 100 var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file) { var fileElems = files.slice(0, maxFileElems).map(function (file) {
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path) var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
@@ -116,10 +112,11 @@ function CreateTorrentPage (state) {
comment: comment comment: comment
} }
dispatch('createTorrent', options) dispatch('createTorrent', options)
dispatch('backToList')
} }
function handleCancel () { function handleCancel () {
dispatch('back') dispatch('backToList')
} }
function handleToggleShowAdvanced () { function handleToggleShowAdvanced () {
@@ -130,27 +127,6 @@ function CreateTorrentPage (state) {
} }
} }
function CreateTorrentErrorPage () {
return hx`
<div class='create-torrent-page'>
<h2>Create torrent</h2>
<p class="torrent-info">
<p>
Sorry, you must select at least one file that is not a hidden file.
</p>
<p>
Hidden files, starting with a . character, are not included.
</p>
</p>
<p class="float-right">
<button class='button-flat light' onclick=${dispatcher('back')}>
Cancel
</button>
</p>
</div>
`
}
// Finds the longest common prefix // Finds the longest common prefix
function findCommonPrefix (a, b) { function findCommonPrefix (a, b) {
for (var i = 0; i < a.length && i < b.length; i++) { for (var i = 0; i < a.length && i < b.length; i++) {

View File

@@ -10,7 +10,7 @@ function Header (state) {
return hx` return hx`
<div class='header'> <div class='header'>
${getTitle()} ${getTitle()}
<div class='nav left float-left'> <div class='nav left'>
<i.icon.back <i.icon.back
class=${state.location.hasBack() ? '' : 'disabled'} class=${state.location.hasBack() ? '' : 'disabled'}
title='Back' title='Back'
@@ -24,7 +24,7 @@ function Header (state) {
chevron_right chevron_right
</i> </i>
</div> </div>
<div class='nav right float-right'> <div class='nav right'>
${getAddButton()} ${getAddButton()}
</div> </div>
</div> </div>
@@ -37,7 +37,7 @@ function Header (state) {
} }
function getAddButton () { function getAddButton () {
if (state.location.url() === 'home') { if (state.location.current().url !== 'player') {
return hx` return hx`
<i <i
class='icon add' class='icon add'

View File

@@ -4,9 +4,9 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx') var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var Bitfield = require('bitfield') var WebChimeraPlayer = require('wcjs-player')
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var zeroFill = require('zero-fill') var Bitfield = require('bitfield')
var TorrentSummary = require('../lib/torrent-summary') var TorrentSummary = require('../lib/torrent-summary')
var {dispatch, dispatcher} = require('../lib/dispatcher') var {dispatch, dispatcher} = require('../lib/dispatcher')
@@ -19,7 +19,6 @@ function Player (state) {
return hx` return hx`
<div <div
class='player' class='player'
onwheel=${handleVolumeWheel}
onmousemove=${dispatcher('mediaMouseMoved')}> onmousemove=${dispatcher('mediaMouseMoved')}>
${showVideo ? renderMedia(state) : renderCastScreen(state)} ${showVideo ? renderMedia(state) : renderCastScreen(state)}
${renderPlayerControls(state)} ${renderPlayerControls(state)}
@@ -27,18 +26,19 @@ function Player (state) {
` `
} }
// Handles volume change by wheel
function handleVolumeWheel (e) {
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
}
function renderMedia (state) { function renderMedia (state) {
if (!state.server) return if (!state.server) return
if (false) return renderMediaTag(state)
else return renderMediaVLC(state)
}
// Renders using a <video> or <audio> tag
// Handles only a subset of codecs, but it's cleaner and more efficient
// See renderMediaVLC()
function renderMediaTag (state) {
// Unfortunately, play/pause can't be done just by modifying HTML. // Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary // Instead, grab the DOM node and play/pause it if necessary
// Get the <video> or <audio> tag var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
var mediaElement = document.querySelector(state.playing.type)
if (mediaElement !== null) { if (mediaElement !== null) {
if (state.playing.isPaused && !mediaElement.paused) { if (state.playing.isPaused && !mediaElement.paused) {
mediaElement.pause() mediaElement.pause()
@@ -50,38 +50,35 @@ function renderMedia (state) {
mediaElement.currentTime = state.playing.jumpToTime mediaElement.currentTime = state.playing.jumpToTime
state.playing.jumpToTime = null state.playing.jumpToTime = null
} }
if (state.playing.playbackRate !== mediaElement.playbackRate) {
mediaElement.playbackRate = state.playing.playbackRate
}
// Set volume // Set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) { if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
mediaElement.volume = state.playing.setVolume mediaElement.volume = state.playing.setVolume
state.playing.setVolume = null state.playing.setVolume = null
} }
// Switch to the newly added subtitle track, if available // fix textTrack cues not been removed <track> rerender
var tracks = mediaElement.textTracks if (state.playing.subtitles.change) {
for (var j = 0; j < tracks.length; j++) { var tracks = mediaElement.textTracks
var isSelectedTrack = j === state.playing.subtitles.selectedIndex for (var j = 0; j < tracks.length; j++) {
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden' // 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
} }
// Save video position state.playing.currentTime = mediaElement.currentTime
var file = state.getPlayingFileSummary() state.playing.duration = mediaElement.duration
file.currentTime = state.playing.currentTime = mediaElement.currentTime
file.duration = state.playing.duration = mediaElement.duration
state.playing.volume = mediaElement.volume state.playing.volume = mediaElement.volume
} }
// Add subtitles to the <video> tag // Add subtitles to the <video> tag
var trackTags = [] 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++) { for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i] var track = state.playing.subtitles.tracks[i]
var isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push(hx` trackTags.push(hx`
<track <track
${isSelected ? 'default' : ''} ${track.selected ? 'default' : ''}
label=${track.label} label=${track.label}
type='subtitles' type='subtitles'
src=${track.buffer}> src=${track.buffer}>
@@ -99,8 +96,7 @@ function renderMedia (state) {
onstalling=${dispatcher('mediaStalled')} onstalling=${dispatcher('mediaStalled')}
onerror=${dispatcher('mediaError')} onerror=${dispatcher('mediaError')}
ontimeupdate=${dispatcher('mediaTimeUpdate')} ontimeupdate=${dispatcher('mediaTimeUpdate')}
onencrypted=${dispatcher('mediaEncrypted')} autoplay>
oncanplay=${onCanPlay}>
${trackTags} ${trackTags}
</div> </div>
` `
@@ -116,7 +112,7 @@ function renderMedia (state) {
</div> </div>
` `
// As soon as we know the video dimensions, resize the window // As soon as the video loads enough to know the video dimensions, resize the window
function onLoadedMetadata (e) { function onLoadedMetadata (e) {
if (state.playing.type !== 'video') return if (state.playing.type !== 'video') return
var video = e.target var video = e.target
@@ -131,17 +127,101 @@ function renderMedia (state) {
function onEnded (e) { function onEnded (e) {
state.playing.isPaused = true state.playing.isPaused = true
} }
}
function onCanPlay (e) { // Renders using WebChimera.js to render using VLC
var elem = e.target // That lets us play media that the <video> tag can't play
if (state.playing.type === 'video' && function renderMediaVLC (state) {
elem.webkitVideoDecodedByteCount === 0) { // Unfortunately, WebChimera can't be done just by modifying HTML.
dispatch('mediaError', 'Video codec unsupported') // Instead, grab the DOM node
} else if (elem.webkitAudioDecodedByteCount === 0) { if (document.querySelector('#media-player')) {
dispatch('mediaError', 'Audio codec unsupported') if (!state.playing.chimera) {
state.playing.chimera = new WebChimeraPlayer('#media-player')
.addPlayer({
autoplay: true,
vlcArgs: ['-vvv'],
wcjsRendererOptions: {'fallbackRenderer': true}
})
.onPlaying(dispatcher('mediaPlaying'))
.onPaused(dispatcher('mediaPaused'))
.onBuffering(dispatcher('mediaStalled'))
.onTime(dispatcher('mediaTimeUpdate'))
.onEnded(onEnded)
.onFrameSetup(onLoadedMetadata)
.addPlaylist(state.server.localURL)
state.playing.chimera.ui(false)
} else { } else {
elem.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
} }
} }
@@ -160,8 +240,7 @@ function renderOverlay (state) {
} else if (elems.length !== 0) { } else if (elems.length !== 0) {
style = { backgroundImage: cssBackgroundImageDarkGradient() } style = { backgroundImage: cssBackgroundImageDarkGradient() }
} else { } else {
// Video playing, so no spinner. No overlay needed return /* Video, not audio, and it isn't stalled, so no spinner. No overlay needed. */
return
} }
return hx` return hx`
@@ -172,7 +251,8 @@ function renderOverlay (state) {
} }
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
var fileSummary = state.getPlayingFileSummary() var torrentSummary = getPlayingTorrentSummary(state)
var fileSummary = torrentSummary.files[state.playing.fileIndex]
if (!fileSummary.audioInfo) return if (!fileSummary.audioInfo) return
var info = fileSummary.audioInfo var info = fileSummary.audioInfo
@@ -191,37 +271,15 @@ function renderAudioMetadata (state) {
track = info.track.no + ' of ' + info.track.of track = info.track.no + ' of ' + info.track.of
} }
// Show a small info box in the middle of the screen with title/album/etc // Show a small info box in the middle of the screen with title/album/artist/etc
var elems = [] var elems = []
if (artist) { if (artist) elems.push(hx`<div class='audio-artist'><label>Artist</label>${artist}</div>`)
elems.push(hx` if (album) elems.push(hx`<div class='audio-album'><label>Album</label>${album}</div>`)
<div class='audio-artist'> if (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`)
<label>Artist</label>${artist}
</div>
`)
}
if (album) {
elems.push(hx`
<div class='audio-album'>
<label>Album</label>${album}
</div>
`)
}
if (track) {
elems.push(hx`
<div class='audio-track'>
<label>Track</label>${track}
</div>
`)
}
// Align the title with the other info, if available. Otherwise, center title // Align the title with the artist/etc info, if available. Otherwise, center the title
var emptyLabel = hx`<label></label>` var emptyLabel = hx`<label></label>`
elems.unshift(hx` elems.unshift(hx`<div class='audio-title'>${elems.length ? emptyLabel : undefined}${title}</div>`)
<div class='audio-title'>
${elems.length ? emptyLabel : undefined}${title}
</div>
`)
return hx`<div class='audio-metadata'>${elems}</div>` return hx`<div class='audio-metadata'>${elems}</div>`
} }
@@ -232,7 +290,7 @@ function renderLoadingSpinner (state) {
(new Date().getTime() - state.playing.lastTimeUpdate > 2000) (new Date().getTime() - state.playing.lastTimeUpdate > 2000)
if (!isProbablyStalled) return if (!isProbablyStalled) return
var prog = state.getPlayingTorrentSummary().progress || {} var prog = getPlayingTorrentSummary(state).progress || {}
var fileProgress = 0 var fileProgress = 0
if (prog.files) { if (prog.files) {
var file = prog.files[state.playing.fileIndex] var file = prog.files[state.playing.fileIndex]
@@ -252,33 +310,20 @@ function renderLoadingSpinner (state) {
} }
function renderCastScreen (state) { function renderCastScreen (state) {
var castIcon, castType, isCast var castIcon, castType
if (state.playing.location.startsWith('chromecast')) { if (state.playing.location.startsWith('chromecast')) {
castIcon = 'cast_connected' castIcon = 'cast_connected'
castType = 'Chromecast' castType = 'Chromecast'
isCast = true
} else if (state.playing.location.startsWith('airplay')) { } else if (state.playing.location.startsWith('airplay')) {
castIcon = 'airplay' castIcon = 'airplay'
castType = 'AirPlay' castType = 'AirPlay'
isCast = true
} else if (state.playing.location.startsWith('dlna')) { } else if (state.playing.location.startsWith('dlna')) {
castIcon = 'tv' castIcon = 'tv'
castType = 'DLNA' castType = 'DLNA'
isCast = true
} else if (state.playing.location === 'vlc') {
castIcon = 'tv'
castType = 'VLC'
isCast = false
} else if (state.playing.location === 'error') {
castIcon = 'error_outline'
castType = 'Error'
isCast = false
} }
var isStarting = state.playing.location.endsWith('-pending') var isStarting = state.playing.location.endsWith('-pending')
var castStatus var castStatus = isStarting ? 'Connecting...' : 'Connected'
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
else castStatus = ''
// Show a nice title image, if possible // Show a nice title image, if possible
var style = { var style = {
@@ -298,37 +343,23 @@ function renderCastScreen (state) {
function renderSubtitlesOptions (state) { function renderSubtitlesOptions (state) {
var subtitles = state.playing.subtitles var subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return if (subtitles.tracks.length && subtitles.show) {
return hx`<ul.subtitles-list>
var items = subtitles.tracks.map(function (track, ix) { ${subtitles.tracks.map(function (w, i) {
var isSelected = state.playing.subtitles.selectedIndex === ix return hx`<li onclick=${dispatcher('selectSubtitle', w.label)}><i.icon>${w.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>${w.label}</li>`
return hx` })}
<li onclick=${dispatcher('selectSubtitle', ix)}> <li onclick=${dispatcher('selectSubtitle', '')}><i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>None</li>
<i.icon>${'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i> </ul>
${track.label}
</li>
` `
}) }
var noneSelected = state.playing.subtitles.selectedIndex === -1
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
return hx`
<ul.subtitles-list>
${items}
<li onclick=${dispatcher('selectSubtitle', -1)}>
<i.icon>${noneClass}</i>
None
</li>
</ul>
`
} }
function renderPlayerControls (state) { function renderPlayerControls (state) {
var positionPercent = 100 * state.playing.currentTime / state.playing.duration var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' } var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0 var captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled' ? 'disabled'
: state.playing.subtitles.selectedIndex >= 0 : state.playing.subtitles.enabled
? 'active' ? 'active'
: '' : ''
@@ -336,27 +367,15 @@ function renderPlayerControls (state) {
hx` hx`
<div class='playback-bar'> <div class='playback-bar'>
${renderLoadingBar(state)} ${renderLoadingBar(state)}
<div <div class='playback-cursor' style=${playbackCursorStyle}></div>
class='playback-cursor' <div class='scrub-bar'
style=${playbackCursorStyle}>
</div>
<div
class='scrub-bar'
draggable='true' draggable='true'
ondragstart=${handleDragStart}
onclick=${handleScrub}, onclick=${handleScrub},
ondrag=${handleScrub}> ondrag=${handleScrub}></div>
</div>
</div> </div>
`, `,
hx` hx`
<i class='icon play-pause float-left' onclick=${dispatcher('playPause')}> <i class='icon fullscreen'
${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>
`,
hx`
<i
class='icon fullscreen float-right'
onclick=${dispatcher('toggleFullScreen')}> onclick=${dispatcher('toggleFullScreen')}>
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'} ${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i> </i>
@@ -366,7 +385,7 @@ function renderPlayerControls (state) {
if (state.playing.type === 'video') { if (state.playing.type === 'video') {
// show closed captions icon // show closed captions icon
elements.push(hx` elements.push(hx`
<i.icon.closed-captions.float-right <i.icon.closed-captions
class=${captionsClass} class=${captionsClass}
onclick=${handleSubtitles}> onclick=${handleSubtitles}>
closed_captions closed_captions
@@ -378,9 +397,7 @@ function renderPlayerControls (state) {
var isOnChromecast = state.playing.location.startsWith('chromecast') var isOnChromecast = state.playing.location.startsWith('chromecast')
var isOnAirplay = state.playing.location.startsWith('airplay') var isOnAirplay = state.playing.location.startsWith('airplay')
var isOnDlna = state.playing.location.startsWith('dlna') var isOnDlna = state.playing.location.startsWith('dlna')
var chromecastClass, chromecastHandler var chromecastClass, chromecastHandler, airplayClass, airplayHandler, dlnaClass, dlnaHandler
var airplayClass, airplayHandler
var dlnaClass, dlnaHandler
if (isOnChromecast) { if (isOnChromecast) {
chromecastClass = 'active' chromecastClass = 'active'
dlnaClass = 'disabled' dlnaClass = 'disabled'
@@ -413,7 +430,7 @@ function renderPlayerControls (state) {
if (state.devices.chromecast || isOnChromecast) { if (state.devices.chromecast || isOnChromecast) {
var castIcon = isOnChromecast ? 'cast_connected' : 'cast' var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
elements.push(hx` elements.push(hx`
<i.icon.device.float-right <i.icon.device
class=${chromecastClass} class=${chromecastClass}
onclick=${chromecastHandler}> onclick=${chromecastHandler}>
${castIcon} ${castIcon}
@@ -422,7 +439,7 @@ function renderPlayerControls (state) {
} }
if (state.devices.airplay || isOnAirplay) { if (state.devices.airplay || isOnAirplay) {
elements.push(hx` elements.push(hx`
<i.icon.device.float-right <i.icon.device
class=${airplayClass} class=${airplayClass}
onclick=${airplayHandler}> onclick=${airplayHandler}>
airplay airplay
@@ -431,8 +448,7 @@ function renderPlayerControls (state) {
} }
if (state.devices.dlna || isOnDlna) { if (state.devices.dlna || isOnDlna) {
elements.push(hx` elements.push(hx`
<i <i.icon.device
class='icon device float-right'
class=${dlnaClass} class=${dlnaClass}
onclick=${dlnaHandler}> onclick=${dlnaHandler}>
tv tv
@@ -440,71 +456,56 @@ function renderPlayerControls (state) {
`) `)
} }
// render volume // On OSX, the back button is in the title bar of the window; see app.js
var volume = state.playing.volume // On other platforms, we render one over the video on mouseover
var volumeIcon = 'volume_' + ( if (process.platform !== 'darwin') {
volume === 0 ? 'off'
: volume < 0.3 ? 'mute'
: volume < 0.6 ? 'down'
: 'up')
var volumeStyle = {
background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' +
'color-stop(' + (volume * 100) + '%, #727272))'
}
elements.push(hx`
<div class='volume float-left'>
<i
class='icon volume-icon float-left'
onmousedown=${handleVolumeMute}>
${volumeIcon}
</i>
<input
class='volume-slider float-right'
type='range' min='0' max='1' step='0.05'
value=${volumeChanging !== false ? volumeChanging : volume}
onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub}
style=${volumeStyle}
/>
</div>
`)
// Show video playback progress
var currentTimeStr = formatTime(state.playing.currentTime)
var durationStr = formatTime(state.playing.duration)
elements.push(hx`
<span class='time float-left'>
${currentTimeStr} / ${durationStr}
</span>
`)
// render playback rate
if (state.playing.playbackRate !== 1) {
elements.push(hx` elements.push(hx`
<span class='rate float-left'> <i.icon.back
${state.playing.playbackRate}x onclick=${dispatcher('back')}>
</span> chevron_left
</i>
`) `)
} }
// render volume
var volume = state.playing.volume
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
var volumeStyle = { background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' +
'color-stop(' + (volume * 100) + '%, #727272))'
}
elements.push(hx`
<div.volume
onwheel=${handleVolumeWheel}>
<i.icon.volume-icon onmousedown=${handleVolumeMute}>
${volumeIcon}
</i>
<input.volume-slider
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub}
onwheel=${handleVolumeWheel}
style=${volumeStyle}
/>
</div>
`)
// Finally, the big button in the center plays or pauses the video
elements.push(hx`
<i class='icon play-pause' onclick=${dispatcher('playPause')}>
${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>
`)
return hx` return hx`
<div class='controls'> <div class='player-controls'>
${elements} ${elements}
${renderSubtitlesOptions(state)} ${renderSubtitlesOptions(state)}
</div> </div>
` `
function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) {
var dt = e.dataTransfer
dt.effectAllowed = 'none'
}
}
// Handles a click or drag to scrub (jump to another position in the video) // Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) { function handleScrub (e) {
dispatch('mediaMouseMoved') dispatch('mediaMouseMoved')
@@ -514,6 +515,11 @@ function renderPlayerControls (state) {
dispatch('playbackJump', position) dispatch('playbackJump', position)
} }
// Handles volume change by wheel
function handleVolumeWheel (e) {
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
}
// Handles volume muting and Unmuting // Handles volume muting and Unmuting
function handleVolumeMute (e) { function handleVolumeMute (e) {
if (state.playing.volume === 0.0) { if (state.playing.volume === 0.0) {
@@ -547,7 +553,7 @@ function renderPlayerControls (state) {
// if no subtitles available select it // if no subtitles available select it
dispatch('openSubtitles') dispatch('openSubtitles')
} else { } else {
dispatch('toggleSubtitlesMenu') dispatch('showSubtitles')
} }
} }
} }
@@ -558,7 +564,7 @@ var volumeChanging = false
// Renders the loading bar. Shows which parts of the torrent are loaded, which // Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be "spongey" / non-contiguous // can be "spongey" / non-contiguous
function renderLoadingBar (state) { function renderLoadingBar (state) {
var torrentSummary = state.getPlayingTorrentSummary() var torrentSummary = getPlayingTorrentSummary(state)
if (!torrentSummary.progress) { if (!torrentSummary.progress) {
return [] return []
} }
@@ -595,7 +601,7 @@ function renderLoadingBar (state) {
// Returns the CSS background-image string for a poster image + dark vignette // Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) { function cssBackgroundImagePoster (state) {
var torrentSummary = state.getPlayingTorrentSummary() var torrentSummary = getPlayingTorrentSummary(state)
var posterPath = TorrentSummary.getPosterPath(torrentSummary) var posterPath = TorrentSummary.getPosterPath(torrentSummary)
if (!posterPath) return '' if (!posterPath) return ''
return cssBackgroundImageDarkGradient() + `, url(${posterPath})` return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
@@ -606,17 +612,7 @@ function cssBackgroundImageDarkGradient () {
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' 'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
} }
function formatTime (time) { function getPlayingTorrentSummary (state) {
if (typeof time !== 'number' || Number.isNaN(time)) { var infoHash = state.playing.infoHash
return '0:00' return state.saved.torrents.find((x) => x.infoHash === infoHash)
}
var hours = Math.floor(time / 3600)
var minutes = Math.floor(time % 3600 / 60)
if (hours > 0) {
minutes = zeroFill(2, minutes)
}
var seconds = zeroFill(2, Math.floor(time % 60))
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
} }

View File

@@ -1,104 +0,0 @@
module.exports = Preferences
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatch} = require('../lib/dispatcher')
var remote = require('electron').remote
var dialog = remote.dialog
function Preferences (state) {
return hx`
<div class='preferences'>
${renderGeneralSection(state)}
</div>
`
}
function renderGeneralSection (state) {
return renderSection({
title: 'General',
description: '',
icon: 'settings'
}, [
renderDownloadDirSelector(state)
])
}
function renderDownloadDirSelector (state) {
return renderFileSelector({
label: 'Download Path',
description: 'Data from torrents will be saved here',
property: 'downloadPath',
options: {
title: 'Select download directory',
properties: [ 'openDirectory' ]
}
},
state.unsaved.prefs.downloadPath,
function (filePath) {
setStateValue('downloadPath', filePath)
})
}
// Renders a prefs section.
// - definition should be {icon, title, description}
// - controls should be an array of vdom elements
function renderSection (definition, controls) {
var helpElem = !definition.description ? null : hx`
<div class='help text'>
<i.icon>help_outline</i>${definition.description}
</div>
`
return hx`
<section class='section preferences-panel'>
<div class='section-container'>
<div class='section-heading'>
<i.icon>${definition.icon}</i>${definition.title}
</div>
${helpElem}
<div class='section-body'>
${controls}
</div>
</div>
</section>
`
}
// Creates a file chooser
// - defition should be {label, description, options}
// options are passed to dialog.showOpenDialog
// - value should be the current pref, a file or folder path
// - callback takes a new file or folder path
function renderFileSelector (definition, value, callback) {
return hx`
<div class='control-group'>
<div class='controls'>
<label class='control-label'>
<div class='preference-title'>${definition.label}</div>
<div class='preference-description'>${definition.description}</div>
</label>
<div class='controls'>
<input type='text' class='file-picker-text'
id=${definition.property}
disabled='disabled'
value=${value} />
<button class='btn' onclick=${handleClick}>
<i.icon>folder_open</i>
</button>
</div>
</div>
</div>
`
function handleClick () {
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
if (!Array.isArray(filenames)) return
callback(filenames[0])
})
}
}
function setStateValue (property, value) {
dispatch('updatePreferences', property, value)
}

View File

@@ -67,48 +67,38 @@ function TorrentList (state) {
// If it's downloading/seeding then show progress info // If it's downloading/seeding then show progress info
var prog = torrentSummary.progress var prog = torrentSummary.progress
if (torrentSummary.status !== 'paused' && prog) { if (torrentSummary.status !== 'paused' && prog) {
var progress = Math.floor(100 * prog.progress)
var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0)
if (downloaded !== total) downloaded += ` / ${total}`
elements.push(hx` elements.push(hx`
<div class='ellipsis'> <div class='status ellipsis'>
${renderPercentProgress()} ${getFilesLength()}
${renderTotalProgress()} <span>${getPeers()}</span>
${renderPeers()} <span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
${renderDownloadSpeed()} <span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
${renderUploadSpeed()} </div>
`)
elements.push(hx`
<div class='status2 ellipsis'>
<span class='progress'>${progress}%</span>
<span>${downloaded}</span>
</div> </div>
`) `)
} }
return hx`<div class='metadata'>${elements}</div>` return hx`<div class='metadata'>${elements}</div>`
function renderPercentProgress () { function getPeers () {
var progress = Math.floor(100 * prog.progress)
return hx`<span>${progress}%</span>`
}
function renderTotalProgress () {
var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0)
if (downloaded === total) {
return hx`<span>${downloaded}</span>`
} else {
return hx`<span>${downloaded} / ${total}</span>`
}
}
function renderPeers () {
if (prog.numPeers === 0) return
var count = prog.numPeers === 1 ? 'peer' : 'peers' var count = prog.numPeers === 1 ? 'peer' : 'peers'
return hx`<span>${prog.numPeers} ${count}</span>` return `${prog.numPeers} ${count}`
} }
function renderDownloadSpeed () { function getFilesLength () {
if (prog.downloadSpeed === 0) return if (torrentSummary.files && torrentSummary.files.length > 1) {
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>` return hx`<span class='files'>${torrentSummary.files.length} files</span>`
} }
function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>`
} }
} }
@@ -118,7 +108,12 @@ function TorrentList (state) {
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'timeout') { if (torrentSummary.playStatus === 'unplayable') {
playIcon = 'play_arrow'
playClass = 'disabled'
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
'View details and click on individual files to open them in another program.'
} else if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning' playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.' playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else { } else {
@@ -138,18 +133,6 @@ function TorrentList (state) {
downloadTooltip = 'Click to start torrenting.' downloadTooltip = 'Click to start torrenting.'
} }
// Do we have a saved position? Show it using a radial progress bar on top
// of the play button, unless already showing a spinner there:
var positionElem
var willShowSpinner = torrentSummary.playStatus === 'requested'
var defaultFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
var fraction = defaultFile.currentTime / defaultFile.duration
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position'
}
// Only show the play button for torrents that contain playable media // Only show the play button for torrents that contain playable media
var playButton var playButton
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) { if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
@@ -165,7 +148,6 @@ function TorrentList (state) {
return hx` return hx`
<div class='buttons'> <div class='buttons'>
${positionElem}
${playButton} ${playButton}
<i.button-round.icon.download <i.button-round.icon.download
class=${torrentSummary.status} class=${torrentSummary.status}
@@ -185,6 +167,7 @@ function TorrentList (state) {
// Show files, per-file download status and play buttons, and so on // Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrentSummary) { function renderTorrentDetails (torrentSummary) {
var infoHash = torrentSummary.infoHash
var filesElement var filesElement
if (!torrentSummary.files) { if (!torrentSummary.files) {
// We don't know what files this torrent contains // We don't know what files this torrent contains
@@ -194,16 +177,15 @@ function TorrentList (state) {
filesElement = hx`<div class='files warning'>${message}</div>` filesElement = hx`<div class='files warning'>${message}</div>`
} else { } else {
// We do know the files. List them and show download stats for each one // We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files var fileRows = torrentSummary.files.map(
.sort(function (a, b) { (file, index) => renderFileRow(torrentSummary, file, index))
if (a.name < b.name) return -1
if (b.name < a.name) return 1
return 0
})
.map((file, index) => renderFileRow(torrentSummary, file, index))
filesElement = hx` filesElement = hx`
<div class='files'> <div class='files'>
<strong>Files</strong>
<span class='open-folder'
onclick=${dispatcher('openFolder', infoHash)}>
Open folder
</span>
<table> <table>
${fileRows} ${fileRows}
</table> </table>
@@ -221,8 +203,7 @@ function TorrentList (state) {
// Show a single torrentSummary file in the details view for a single torrent // Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrentSummary, file, index) { function renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded // First, find out how much of the file we've downloaded
var isSelected = torrentSummary.selections[index] // Are we even torrenting it? var isDone = false
var isDone = false // Are we finished torrenting it?
var progress = '' var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files) { if (torrentSummary.progress && torrentSummary.progress.files) {
var fileProg = torrentSummary.progress.files[index] var fileProg = torrentSummary.progress.files[index]
@@ -230,69 +211,28 @@ function TorrentList (state) {
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%' progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
} }
// Second, for media files where we saved our position, show how far we got // Second, render the file as a table row
var positionElem
if (file.currentTime) {
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
positionElem = renderRadialProgressBar(file.currentTime / file.duration)
}
// Finally, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var icon var icon
var rowClass = ''
var handleClick var handleClick
if (isPlayable) { if (TorrentPlayer.isPlayable(file)) {
icon = 'play_arrow' /* playable? add option to play */ icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index) handleClick = dispatcher('play', infoHash, index)
} else { } else {
icon = 'description' /* file icon, opens in OS default app */ icon = 'description' /* file icon, opens in OS default app */
rowClass = isDone ? '' : 'disabled'
handleClick = dispatcher('openFile', infoHash, index) 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` return hx`
<tr onclick=${handleClick}> <tr onclick=${handleClick} class='${rowClass}'>
<td class='col-icon ${rowClass}'> <td class='col-icon'>
${positionElem}
<i class='icon'>${icon}</i> <i class='icon'>${icon}</i>
</td> </td>
<td class='col-name ${rowClass}'> <td class='col-name'>${file.name}</td>
${file.name} <td class='col-progress'>${progress}</td>
</td> <td class='col-size'>${prettyBytes(file.length)}</td>
<td class='col-progress ${rowClass}'>
${isSelected ? progress : ''}
</td>
<td class='col-size ${rowClass}'>
${prettyBytes(file.length)}
</td>
<td class='col-select'
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
</td>
</tr> </tr>
` `
} }
} }
function renderRadialProgressBar (fraction, cssClass) {
var rotation = 360 * fraction
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return hx`
<div class="radial-progress ${cssClass}">
<div class="circle">
<div class="mask full" style=${transformFill}>
<div class="fill" style=${transformFill}></div>
</div>
<div class="mask half">
<div class="fill" style=${transformFill}></div>
<div class="fill fix" style=${transformFix}></div>
</div>
</div>
<div class="inset"></div>
</div>
`
}

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,16 +28,7 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid // Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq // client, as explained here: https://webtorrent.io/faq
var client = window.client = new WebTorrent({ var client = window.client = new WebTorrent()
tracker: {
// HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome bug.
// Fixed in Chrome 51, so we can remove this hack once Electron updates Chrome.
// Issue: https://github.com/feross/webtorrent-desktop/issues/353
// HACK #2: Windows: Disable WebRTC to fix Chrome 50 / Electron 1.1.[1-3] crash.
// Issue: https://github.com/electron/electron/issues/5629
wrtc: process.platform === 'linux'
}
})
// WebTorrent-to-HTTP streaming sever // WebTorrent-to-HTTP streaming sever
var server = window.server = null var server = window.server = null
@@ -51,8 +42,8 @@ function init () {
client.on('warning', (err) => ipc.send('wt-warning', null, err.message)) client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', 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) => ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections)) startTorrenting(torrentKey, torrentID, path, fileModtimes))
ipc.on('wt-stop-torrenting', (e, infoHash) => ipc.on('wt-stop-torrenting', (e, infoHash) =>
stopTorrenting(infoHash)) stopTorrenting(infoHash))
ipc.on('wt-create-torrent', (e, torrentKey, options) => ipc.on('wt-create-torrent', (e, torrentKey, options) =>
@@ -67,8 +58,6 @@ function init () {
startServer(infoHash, index)) startServer(infoHash, index))
ipc.on('wt-stop-server', (e) => ipc.on('wt-stop-server', (e) =>
stopServer()) stopServer())
ipc.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections))
ipc.send('ipcReadyWebTorrent') ipc.send('ipcReadyWebTorrent')
@@ -77,27 +66,31 @@ function init () {
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object // 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- // 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) console.log('starting torrent %s: %s', torrentKey, torrentID)
var torrent
var torrent = client.add(torrentID, { try {
path: path, torrent = client.add(torrentID, {
fileModtimes: fileModtimes path: path,
}) fileModtimes: fileModtimes
})
} catch (err) {
return ipc.send('wt-error', torrentKey, err.message)
}
// If we add a duplicate magnet URI or infohash, WebTorrent returns the
// existing torrent object! (If we add a duplicate torrent file, it creates a
// new torrent object and raises an error later.) Workaround:
if (torrent.key) {
return ipc.send('wt-error', torrentKey, 'Can\'t add duplicate torrent')
}
torrent.key = torrentKey torrent.key = torrentKey
// Listen for ready event, progress notifications, etc
addTorrentEvents(torrent) addTorrentEvents(torrent)
// Only download the files the user wants, not necessarily all files
torrent.once('ready', () => selectFiles(torrent, selections))
return torrent return torrent
} }
function stopTorrenting (infoHash) { function stopTorrenting (infoHash) {
var torrent = client.get(infoHash) var torrent = client.get(infoHash)
if (torrent) torrent.destroy() torrent.destroy()
} }
// Create a new torrent, start seeding // Create a new torrent, start seeding
@@ -166,7 +159,9 @@ function getTorrentFileInfo (file) {
return { return {
name: file.name, name: file.name,
length: file.length, length: file.length,
path: file.path path: file.path,
numPiecesPresent: 0,
numPieces: null
} }
} }
@@ -317,44 +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()
}
}
}
// Gets a WebTorrent handle by torrentKey // Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key // Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) { function getTorrent (torrentKey) {