Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8cb6abf0a | ||
|
|
94b3bc561d | ||
|
|
5eb75d0250 | ||
|
|
b577e08053 | ||
|
|
dae4840bd6 | ||
|
|
57eb52a606 | ||
|
|
6d670bdd3f | ||
|
|
def2209dc5 | ||
|
|
763c573c7a | ||
|
|
eb61f2ac0e | ||
|
|
a9d1925686 | ||
|
|
0e10eba073 | ||
|
|
0427e1f3a6 | ||
|
|
c841c94784 | ||
|
|
e3c6049fdb | ||
|
|
829206e921 | ||
|
|
f7acdffb2a | ||
|
|
cc9ba385bf | ||
|
|
e88ddd648b | ||
|
|
dac34541d6 | ||
|
|
52fb378fd5 | ||
|
|
8fc61a1c90 | ||
|
|
04691ed0da | ||
|
|
f9d4e5e077 | ||
|
|
4ee36f459f | ||
|
|
2c0de25423 | ||
|
|
c82bdbd39d | ||
|
|
71b08304f2 | ||
|
|
3bb3cd7c44 | ||
|
|
41187ec43d | ||
|
|
cf5de49deb | ||
|
|
19f177f3ee | ||
|
|
556d0cb1c5 | ||
|
|
7c7780b17e | ||
|
|
bd358b7692 | ||
|
|
1b8f180255 | ||
|
|
0bc90cea21 | ||
|
|
10f96ab23e | ||
|
|
4f0df507f4 | ||
|
|
256753e6ff | ||
|
|
8ac42078d4 | ||
|
|
fc83e054ea | ||
|
|
62cb304971 | ||
|
|
d4efebd694 | ||
|
|
7833f6bbc4 | ||
|
|
8b773c5f59 | ||
|
|
5767d5b95d | ||
|
|
13f1ecdbe3 | ||
|
|
8ae4ac47e6 | ||
|
|
001601bc5f | ||
|
|
3757507b18 | ||
|
|
9abab7aec3 | ||
|
|
1aabd537d8 | ||
|
|
6e240b3fd4 | ||
|
|
501a07c386 | ||
|
|
0d92dee14e | ||
|
|
3a1fa25106 | ||
|
|
b167770ea6 | ||
|
|
2a8a26ac54 | ||
|
|
9748833ba9 | ||
|
|
bf49214790 | ||
|
|
2b4410a55a | ||
|
|
bfd1b2eaf0 | ||
|
|
44c3421e92 | ||
|
|
7de3d3cc41 | ||
|
|
3d7f46da65 | ||
|
|
72d902e548 | ||
|
|
955fe76c3c | ||
|
|
839bec0363 | ||
|
|
9af4ce9a6b | ||
|
|
205bf75c7e | ||
|
|
bafbf3d841 | ||
|
|
1b0833fb45 | ||
|
|
0a15db2892 | ||
|
|
63dda10380 | ||
|
|
6e651df083 | ||
|
|
3a8fe24eec | ||
|
|
918a35e091 | ||
|
|
c76abeb8c0 | ||
|
|
d389b8ab38 | ||
|
|
a59faacbd7 | ||
|
|
12f9709601 | ||
|
|
455c9c02b9 | ||
|
|
1b49c6568b | ||
|
|
30e81c7699 | ||
|
|
2dafc68301 | ||
|
|
c310222af2 | ||
|
|
b4bb9a6603 | ||
|
|
279c621d23 | ||
|
|
eb11dbdcbd | ||
|
|
8dfdb34d31 | ||
|
|
fc9a73d67f | ||
|
|
4b5b84a0fc | ||
|
|
327c95d754 | ||
|
|
6e969e5d07 | ||
|
|
ca7c872420 | ||
|
|
8af4f42c42 | ||
|
|
ffce76a9b1 | ||
|
|
fca1d9dae4 | ||
|
|
eba09430e3 | ||
|
|
6bc8de7625 | ||
|
|
8a08ed8538 | ||
|
|
56d802f741 | ||
|
|
f7b46336fd | ||
|
|
510187c2ae | ||
|
|
ff6ff8db00 | ||
|
|
014017604d | ||
|
|
8cf544d54f | ||
|
|
870dd893fc | ||
|
|
bf3b9ced74 | ||
|
|
9ecc12fb7f | ||
|
|
aafb1421c6 | ||
|
|
76c732bafb | ||
|
|
ab476c9a9c | ||
|
|
4470310814 | ||
|
|
b6ba4f45c8 | ||
|
|
84c860cfcb | ||
|
|
47c554a5ff | ||
|
|
4e46b16c13 | ||
|
|
22cdcdb468 | ||
|
|
f238b2d105 | ||
|
|
3a81799828 | ||
|
|
5dca89b61c | ||
|
|
264c035ef7 | ||
|
|
8f39f8a23e | ||
|
|
a29dbd7a71 | ||
|
|
60a8969abc | ||
|
|
9747d28514 | ||
|
|
17ccd217a9 | ||
|
|
0df6198549 | ||
|
|
74ada99f2b | ||
|
|
81d5a367da | ||
|
|
189e4bdc24 | ||
|
|
7bd30f8a16 | ||
|
|
7c6b7e4a6d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
dist
|
||||
@@ -10,7 +10,7 @@
|
||||
- Romain Beaumont <romain.rom1@gmail.com>
|
||||
- Dan Flettre <fletd01@yahoo.com>
|
||||
- Liam Gray <liam.r.gray@gmail.com>
|
||||
- grunjol <grunjol@argenteam.net>
|
||||
- grunjol <grunjol@users.noreply.github.com>
|
||||
- Rémi Jouannet <remijouannet@users.noreply.github.com>
|
||||
- Evan Miller <miller.evan815@gmail.com>
|
||||
- Alex <alxmorais8@msn.com>
|
||||
@@ -18,5 +18,9 @@
|
||||
- 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>
|
||||
- Thomas Watson Steen <w@tson.dk>
|
||||
|
||||
#### Generated by bin/update-authors.sh.
|
||||
|
||||
81
CHANGELOG.md
81
CHANGELOG.md
@@ -1,5 +1,86 @@
|
||||
# WebTorrent Desktop Version History
|
||||
|
||||
## v0.7.0 - 2016-06-02
|
||||
|
||||
### Added
|
||||
|
||||
- Improved AirPlay support -- using the new [`airplayer`](https://www.npmjs.com/package/airplayer) package
|
||||
- Remember volume setting in player, for as long as the app is open
|
||||
|
||||
### Changed
|
||||
|
||||
- Add (+) button now also accepts non .torrent files and creates a torrent from
|
||||
those files
|
||||
- Show prompt text in title bar for open dialogs (OS X)
|
||||
- Upgrade Electron to 1.2.1
|
||||
- Improve window resizing when aspect ratio is enforced (OS X)
|
||||
- Use .ico format for better icon rendering quality (Windows)
|
||||
- Fix crash reporter not working (Windows)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Re-enable WebRTC (web peers)! (OS X, Windows)
|
||||
- Windows support was disabled in v0.6.1 to work around a bug in Electron
|
||||
- OS X support was disabled in v0.4.0 to work around a 100% CPU bug
|
||||
- Fix subtitle selector radio button UI size glitch
|
||||
- Fix race condition causing exeption on app startup
|
||||
- Fix duplicate torrent detection in some cases
|
||||
- Fix "gray screen" exception caused by incorrect file list order
|
||||
- Fix torrent loading message UI misalignment
|
||||
|
||||
### Known issues
|
||||
|
||||
- When upgrading to WebTorrent Desktop v0.7.0, some torrent metadata (file list,
|
||||
selected files, whether torrent is streamable) will be cleared. Just start the
|
||||
torrent to re-populate the metadata.
|
||||
|
||||
## v0.6.1 - 2016-05-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Disable WebRTC to work around Electron crash (Windows)
|
||||
- Will be re-enabled in the next version of WebTorrent, which will be based on
|
||||
the next version of Electron, where the bug is fixed.
|
||||
- Fix crash when updating from WebTorrent 0.5.x in some situtations (#583)
|
||||
- Fix crash when dropping files onto the dock icon (OS X)
|
||||
- Fix keyboard shortcuts Space and ESC being captured globally (#585)
|
||||
- Fix crash, show error when drag-dropping hidden files (#586)
|
||||
|
||||
## v0.6.0 - 2016-05-24
|
||||
|
||||
### Added
|
||||
|
||||
- Added Preferences page to set Download folder
|
||||
- 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 `instant.io` links (#559)
|
||||
- Add announcement feature
|
||||
|
||||
### Changed
|
||||
|
||||
- Nicer player UI
|
||||
- Reduce startup jank, improve startup time (#568)
|
||||
- Cleanup unsupported codec detection (#569, #570)
|
||||
- Cleaner look for the torrent file list
|
||||
- Improve subtitle positioning (#551)
|
||||
|
||||
### 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
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Feross Aboukhadijeh
|
||||
Copyright (c) WebTorrent, LLC
|
||||
|
||||
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
|
||||
|
||||
@@ -87,4 +87,4 @@ brew install wine
|
||||
|
||||
## License
|
||||
|
||||
MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org).
|
||||
MIT. Copyright (c) [WebTorrent, LLC](https://webtorrent.io).
|
||||
|
||||
@@ -3,49 +3,52 @@
|
||||
var fs = require('fs')
|
||||
var cp = require('child_process')
|
||||
|
||||
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen']
|
||||
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path']
|
||||
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
|
||||
// Scans codebase for missing or unused dependencies. Exits with code 0 on success.
|
||||
function main () {
|
||||
if (process.platform === 'win32') {
|
||||
console.log('Sorry, check-deps only works on Mac and Linux')
|
||||
console.error('Sorry, check-deps only works on Mac and Linux')
|
||||
return
|
||||
}
|
||||
|
||||
var jsDeps = findJSDeps()
|
||||
var usedDeps = findUsedDeps()
|
||||
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)
|
||||
var missingDeps = usedDeps.filter(
|
||||
(dep) => !packageDeps.includes(dep) && !BUILT_IN_DEPS.includes(dep)
|
||||
)
|
||||
var unusedDeps = packageDeps.filter(
|
||||
(dep) => !usedDeps.includes(dep) && !EXECUTABLE_DEPS.includes(dep)
|
||||
)
|
||||
|
||||
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!')
|
||||
if (missingDeps.length > 0) {
|
||||
console.error('Missing package dependencies: ' + missingDeps)
|
||||
}
|
||||
if (unusedDeps.length > 0) {
|
||||
console.error('Unused package dependencies: ' + unusedDeps)
|
||||
}
|
||||
if (missingDeps.length + unusedDeps.length > 0) {
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Finds all dependencies, required, optional, or dev, in package.json
|
||||
// Finds all dependencies specified in `package.json`
|
||||
function findPackageDeps () {
|
||||
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||
var requiredDeps = Object.keys(pkg.dependencies)
|
||||
|
||||
var deps = Object.keys(pkg.dependencies)
|
||||
var devDeps = Object.keys(pkg.devDependencies)
|
||||
var optionalDeps = Object.keys(pkg.optionalDependencies)
|
||||
|
||||
return [].concat(requiredDeps, devDeps, optionalDeps)
|
||||
return [].concat(deps, devDeps, optionalDeps)
|
||||
}
|
||||
|
||||
// Finds all dependencies required() in the code
|
||||
function findJSDeps () {
|
||||
// Finds all dependencies that used with `require()`
|
||||
function findUsedDeps () {
|
||||
var stdout = cp.execSync('./bin/list-deps.sh')
|
||||
return stdout.toString().trim().split('\n')
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ var path = require('path')
|
||||
|
||||
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
|
||||
child.on('close', function (code) {
|
||||
process.exit(code)
|
||||
process.exitCode = code
|
||||
})
|
||||
|
||||
@@ -182,8 +182,6 @@ function buildDarwin (cb) {
|
||||
var infoPlistPath = path.join(contentsPath, 'Info.plist')
|
||||
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 = [
|
||||
{
|
||||
CFBundleTypeExtensions: [ 'torrent' ],
|
||||
@@ -211,6 +209,25 @@ 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))
|
||||
|
||||
// Copy torrent file icon into app bundle
|
||||
@@ -310,11 +327,11 @@ function buildDarwin (cb) {
|
||||
}
|
||||
|
||||
var dmg = appDmg(dmgOpts)
|
||||
dmg.on('error', cb)
|
||||
dmg.once('error', cb)
|
||||
dmg.on('progress', function (info) {
|
||||
if (info.type === 'step-begin') console.log(info.title + '...')
|
||||
})
|
||||
dmg.on('finish', function (info) {
|
||||
dmg.once('finish', function (info) {
|
||||
console.log('OS X: Created dmg.')
|
||||
cb(null)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
npm run update-authors
|
||||
git diff --exit-code
|
||||
npm run package -- --sign
|
||||
git push
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
set -e
|
||||
|
||||
git pull
|
||||
npm run update-authors
|
||||
git diff --exit-code
|
||||
rm -rf node_modules/
|
||||
npm install
|
||||
npm dedupe
|
||||
|
||||
@@ -10,7 +10,6 @@ while (<>) {
|
||||
next if $seen{$_};
|
||||
next if /<support\@greenkeeper.io>/;
|
||||
next if /<ungoldman\@gmail.com>/;
|
||||
next if /<grunjol\@users.noreply.github.com>/;
|
||||
next if /<dc\@DCs-MacBook.local>/;
|
||||
next if /<rolandoguedes\@gmail.com>/;
|
||||
$seen{$_} = push @authors, "- ", $_;
|
||||
|
||||
@@ -3,12 +3,14 @@ var fs = require('fs')
|
||||
var path = require('path')
|
||||
|
||||
var APP_NAME = 'WebTorrent'
|
||||
var APP_TEAM = 'The WebTorrent Project'
|
||||
var APP_TEAM = 'WebTorrent, LLC'
|
||||
var APP_VERSION = require('./package.json').version
|
||||
|
||||
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||
|
||||
module.exports = {
|
||||
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||
|
||||
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
||||
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
||||
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
|
||||
@@ -17,8 +19,7 @@ module.exports = {
|
||||
APP_VERSION: APP_VERSION,
|
||||
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
||||
|
||||
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' +
|
||||
'?version=' + APP_VERSION + '&platform=' + process.platform,
|
||||
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
|
||||
|
||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||
|
||||
|
||||
57
main/announcement.js
Normal file
57
main/announcement.js
Normal file
@@ -0,0 +1,57 @@
|
||||
module.exports = {
|
||||
init
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
|
||||
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
||||
'?version=' + config.APP_VERSION +
|
||||
'&platform=' + process.platform
|
||||
|
||||
/**
|
||||
* In certain situations, the WebTorrent team may need to show an announcement to
|
||||
* all WebTorrent Desktop users. For example: a security notice, or an update
|
||||
* notification (if the auto-updater stops working).
|
||||
*
|
||||
* When there is an announcement, the `ANNOUNCEMENT_URL` endpoint should return an
|
||||
* HTTP 200 status code with a JSON object like this:
|
||||
*
|
||||
* {
|
||||
* "title": "WebTorrent Desktop Announcement",
|
||||
* "message": "Security Issue in v0.xx",
|
||||
* "detail": "Please update to v0.xx as soon as possible..."
|
||||
* }
|
||||
*/
|
||||
function init () {
|
||||
var get = require('simple-get')
|
||||
get.concat(ANNOUNCEMENT_URL, onResponse)
|
||||
}
|
||||
|
||||
function onResponse (err, res, data) {
|
||||
if (err) return log(`Failed to retrieve announcement: ${err.message}`)
|
||||
if (res.statusCode !== 200) return log('No announcement exists')
|
||||
|
||||
try {
|
||||
data = JSON.parse(data.toString())
|
||||
} catch (err) {
|
||||
// Support plaintext announcement messages, using a default title.
|
||||
data = {
|
||||
title: 'WebTorrent Desktop Announcement',
|
||||
message: data.toString(),
|
||||
detail: data.toString()
|
||||
}
|
||||
}
|
||||
|
||||
electron.dialog.showMessageBox({
|
||||
type: 'info',
|
||||
buttons: ['OK'],
|
||||
title: data.title,
|
||||
message: data.message,
|
||||
detail: data.detail
|
||||
}, noop)
|
||||
}
|
||||
|
||||
function noop () {}
|
||||
122
main/dialog.js
Normal file
122
main/dialog.js
Normal file
@@ -0,0 +1,122 @@
|
||||
module.exports = {
|
||||
openSeedFile,
|
||||
openSeedDirectory,
|
||||
openTorrentFile,
|
||||
openTorrentAddress,
|
||||
openFiles
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
var windows = require('./windows')
|
||||
|
||||
/**
|
||||
* Show open dialog to create a single-file torrent.
|
||||
*/
|
||||
function openSeedFile () {
|
||||
if (!windows.main.win) return
|
||||
log('openSeedFile')
|
||||
var opts = {
|
||||
title: 'Select a file for the torrent.',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Show open dialog to create a single-file or single-directory torrent. On
|
||||
* Windows and Linux, open dialogs are for files *or* directories only, not both,
|
||||
* so this function shows a directory dialog on those platforms.
|
||||
*/
|
||||
function openSeedDirectory () {
|
||||
if (!windows.main.win) return
|
||||
log('openSeedDirectory')
|
||||
var opts = process.platform === 'darwin'
|
||||
? {
|
||||
title: 'Select a file or folder for the torrent.',
|
||||
properties: [ 'openFile', 'openDirectory' ]
|
||||
}
|
||||
: {
|
||||
title: 'Select a folder for the torrent.',
|
||||
properties: [ 'openDirectory' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Show flexible open dialog that supports selecting .torrent files to add, or
|
||||
* a file or folder to create a single-file or single-directory torrent.
|
||||
*/
|
||||
function openFiles () {
|
||||
if (!windows.main.win) return
|
||||
log('openFiles')
|
||||
var opts = process.platform === 'darwin'
|
||||
? {
|
||||
title: 'Select a file or folder to add.',
|
||||
properties: [ 'openFile', 'openDirectory' ]
|
||||
}
|
||||
: {
|
||||
title: 'Select a file to add.',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('onOpen', selectedPaths)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Show open dialog to open a .torrent file.
|
||||
*/
|
||||
function openTorrentFile () {
|
||||
if (!windows.main.win) return
|
||||
log('openTorrentFile')
|
||||
var opts = {
|
||||
title: 'Select a .torrent file.',
|
||||
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
||||
properties: [ 'openFile', 'multiSelections' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
selectedPaths.forEach(function (selectedPath) {
|
||||
windows.main.dispatch('addTorrent', selectedPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.)
|
||||
*/
|
||||
function openTorrentAddress () {
|
||||
log('openTorrentAddress')
|
||||
windows.main.dispatch('openTorrentAddress')
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialogs on do not show a title on OS X, so the window title is used instead.
|
||||
*/
|
||||
function setTitle (title) {
|
||||
if (process.platform === 'darwin') {
|
||||
windows.main.dispatch('setTitle', title)
|
||||
}
|
||||
}
|
||||
|
||||
function resetTitle () {
|
||||
setTitle(config.APP_WINDOW_TITLE)
|
||||
}
|
||||
59
main/dock.js
Normal file
59
main/dock.js
Normal file
@@ -0,0 +1,59 @@
|
||||
module.exports = {
|
||||
downloadFinished,
|
||||
init,
|
||||
setBadge
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
|
||||
var dialog = require('./dialog')
|
||||
var log = require('./log')
|
||||
|
||||
/**
|
||||
* Add a right-click menu to the dock icon. (OS X)
|
||||
*/
|
||||
function init () {
|
||||
if (!app.dock) return
|
||||
var menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
app.dock.setMenu(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounce the Downloads stack if `path` is inside the Downloads folder. (OS X)
|
||||
*/
|
||||
function downloadFinished (path) {
|
||||
if (!app.dock) return
|
||||
log(`downloadFinished: ${path}`)
|
||||
app.dock.downloadFinished(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Display string in dock badging area. (OS X)
|
||||
*/
|
||||
function setBadge (text) {
|
||||
if (!app.dock) return
|
||||
log(`setBadge: ${text}`)
|
||||
app.dock.setBadge(String(text))
|
||||
}
|
||||
|
||||
function getMenuTemplate () {
|
||||
return [
|
||||
{
|
||||
label: 'Create New Torrent...',
|
||||
accelerator: 'CmdOrCtrl+N',
|
||||
click: () => dialog.openSeedDirectory()
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent File...',
|
||||
accelerator: 'CmdOrCtrl+O',
|
||||
click: () => dialog.openTorrentFile()
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent Address...',
|
||||
accelerator: 'CmdOrCtrl+U',
|
||||
click: () => dialog.openTorrentAddress()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,9 +3,8 @@ module.exports = {
|
||||
uninstall
|
||||
}
|
||||
|
||||
var path = require('path')
|
||||
|
||||
var config = require('../config')
|
||||
var path = require('path')
|
||||
|
||||
function install () {
|
||||
if (process.platform === 'darwin') {
|
||||
@@ -35,11 +34,11 @@ function installDarwin () {
|
||||
var electron = require('electron')
|
||||
var app = electron.app
|
||||
|
||||
// On OS X, only protocols that are listed in Info.plist can be set as the default
|
||||
// handler at runtime.
|
||||
// On OS X, only protocols that are listed in `Info.plist` can be set as the
|
||||
// default handler at runtime.
|
||||
app.setAsDefaultProtocolClient('magnet')
|
||||
|
||||
// File handlers are registered in the Info.plist.
|
||||
// File handlers are defined in `Info.plist`.
|
||||
}
|
||||
|
||||
function uninstallDarwin () {}
|
||||
@@ -55,10 +54,22 @@ function installWin32 () {
|
||||
|
||||
var log = require('./log')
|
||||
|
||||
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
||||
|
||||
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
|
||||
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND)
|
||||
var iconPath = path.join(
|
||||
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
|
||||
)
|
||||
registerProtocolHandlerWin32(
|
||||
'magnet',
|
||||
'URL:BitTorrent Magnet URL',
|
||||
iconPath,
|
||||
EXEC_COMMAND
|
||||
)
|
||||
registerFileHandlerWin32(
|
||||
'.torrent',
|
||||
'io.webtorrent.torrent',
|
||||
'BitTorrent Document',
|
||||
iconPath,
|
||||
EXEC_COMMAND
|
||||
)
|
||||
|
||||
/**
|
||||
* To add a protocol handler, the following keys must be added to the Windows registry:
|
||||
@@ -265,7 +276,9 @@ function installLinux () {
|
||||
installIconFile()
|
||||
|
||||
function installDesktopFile () {
|
||||
var templatePath = path.join(config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop')
|
||||
var templatePath = path.join(
|
||||
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
|
||||
)
|
||||
fs.readFile(templatePath, 'utf8', writeDesktopFile)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ var electron = require('electron')
|
||||
var app = electron.app
|
||||
var ipcMain = electron.ipcMain
|
||||
|
||||
var announcement = require('./announcement')
|
||||
var config = require('../config')
|
||||
var crashReporter = require('../crash-reporter')
|
||||
var dialog = require('./dialog')
|
||||
var dock = require('./dock')
|
||||
var handlers = require('./handlers')
|
||||
var ipc = require('./ipc')
|
||||
var log = require('./log')
|
||||
var menu = require('./menu')
|
||||
var shortcuts = require('./shortcuts')
|
||||
var squirrelWin32 = require('./squirrel-win32')
|
||||
var tray = require('./tray')
|
||||
var updater = require('./updater')
|
||||
@@ -53,23 +55,22 @@ function init () {
|
||||
|
||||
ipc.init()
|
||||
|
||||
app.on('will-finish-launching', function () {
|
||||
app.once('will-finish-launching', function () {
|
||||
crashReporter.init()
|
||||
})
|
||||
|
||||
app.on('ready', function () {
|
||||
isReady = true
|
||||
|
||||
windows.createMainWindow()
|
||||
windows.createWebTorrentHiddenWindow()
|
||||
windows.main.init()
|
||||
windows.webtorrent.init()
|
||||
menu.init()
|
||||
shortcuts.init()
|
||||
|
||||
// To keep app startup fast, some code is delayed.
|
||||
setTimeout(delayedInit, config.DELAYED_INIT)
|
||||
})
|
||||
|
||||
app.on('ipcReady', function () {
|
||||
app.once('ipcReady', function () {
|
||||
log('Command line args:', argv)
|
||||
processArgv(argv)
|
||||
console.timeEnd('init')
|
||||
@@ -80,19 +81,21 @@ function init () {
|
||||
|
||||
app.isQuitting = true
|
||||
e.preventDefault()
|
||||
windows.main.send('dispatch', 'saveState') /* try to save state on exit */
|
||||
windows.main.dispatch('saveState') // try to save state on exit
|
||||
ipcMain.once('savedState', () => app.quit())
|
||||
setTimeout(() => app.quit(), 2000) /* quit after 2 secs, at most */
|
||||
setTimeout(() => app.quit(), 2000) // quit after 2 secs, at most
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
if (isReady) windows.createMainWindow()
|
||||
if (isReady) windows.main.show()
|
||||
})
|
||||
}
|
||||
|
||||
function delayedInit () {
|
||||
tray.init()
|
||||
announcement.init()
|
||||
dock.init()
|
||||
handlers.install()
|
||||
tray.init()
|
||||
updater.init()
|
||||
}
|
||||
|
||||
@@ -100,13 +103,12 @@ function onOpen (e, torrentId) {
|
||||
e.preventDefault()
|
||||
|
||||
if (app.ipcReady) {
|
||||
windows.main.send('dispatch', 'onOpen', torrentId)
|
||||
// Magnet links opened from Chrome won't focus the app without a setTimeout. The
|
||||
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
|
||||
// Magnet links opened from Chrome won't focus the app without a setTimeout.
|
||||
// The confirmation dialog Chrome shows causes Chrome to steal back the focus.
|
||||
// Electron issue: https://github.com/atom/electron/issues/4338
|
||||
setTimeout(function () {
|
||||
windows.focusWindow(windows.main)
|
||||
}, 100)
|
||||
setTimeout(() => windows.main.show(), 100)
|
||||
|
||||
processArgv([ torrentId ])
|
||||
} else {
|
||||
argv.push(torrentId)
|
||||
}
|
||||
@@ -117,7 +119,7 @@ function onAppOpen (newArgv) {
|
||||
|
||||
if (app.ipcReady) {
|
||||
log('Second app instance opened, but was prevented:', newArgv)
|
||||
windows.focusWindow(windows.main)
|
||||
windows.main.show()
|
||||
|
||||
processArgv(newArgv)
|
||||
} else {
|
||||
@@ -130,18 +132,22 @@ function sliceArgv (argv) {
|
||||
}
|
||||
|
||||
function processArgv (argv) {
|
||||
var torrentIds = []
|
||||
argv.forEach(function (arg) {
|
||||
if (arg === '-n') {
|
||||
menu.showOpenSeedFiles()
|
||||
dialog.openSeedDirectory()
|
||||
} else if (arg === '-o') {
|
||||
menu.showOpenTorrentFile()
|
||||
dialog.openTorrentFile()
|
||||
} else if (arg === '-u') {
|
||||
menu.showOpenTorrentAddress()
|
||||
dialog.openTorrentAddress()
|
||||
} else if (arg.startsWith('-psn')) {
|
||||
// Ignore OS X launchd "process serial number" argument
|
||||
// More: https://github.com/feross/webtorrent-desktop/issues/214
|
||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
||||
} else {
|
||||
windows.main.send('dispatch', 'onOpen', arg)
|
||||
torrentIds.push(arg)
|
||||
}
|
||||
})
|
||||
if (torrentIds.length > 0) {
|
||||
windows.main.dispatch('onOpen', torrentIds)
|
||||
}
|
||||
}
|
||||
|
||||
216
main/ipc.js
216
main/ipc.js
@@ -5,31 +5,32 @@ module.exports = {
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
var ipcMain = electron.ipcMain
|
||||
|
||||
var dialog = require('./dialog')
|
||||
var dock = require('./dock')
|
||||
var log = require('./log')
|
||||
var menu = require('./menu')
|
||||
var windows = require('./windows')
|
||||
var powerSaveBlocker = require('./power-save-blocker')
|
||||
var shell = require('./shell')
|
||||
var shortcuts = require('./shortcuts')
|
||||
var vlc = require('./vlc')
|
||||
var windows = require('./windows')
|
||||
|
||||
// has to be a number, not a boolean, and undefined throws an error
|
||||
var powerSaveBlockerId = 0
|
||||
|
||||
// messages from the main process, to be sent once the WebTorrent process starts
|
||||
// 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 () {
|
||||
ipcMain.on('ipcReady', function (e) {
|
||||
windows.main.show()
|
||||
var ipc = electron.ipcMain
|
||||
|
||||
ipc.once('ipcReady', function (e) {
|
||||
app.ipcReady = true
|
||||
app.emit('ipcReady')
|
||||
})
|
||||
|
||||
ipcMain.on('ipcReadyWebTorrent', function (e) {
|
||||
ipc.once('ipcReadyWebTorrent', function (e) {
|
||||
app.ipcReadyWebTorrent = true
|
||||
log('sending %d queued messages from the main win to the webtorrent window',
|
||||
messageQueueMainToWebTorrent.length)
|
||||
@@ -39,113 +40,112 @@ function init () {
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
|
||||
/**
|
||||
* Dialog
|
||||
*/
|
||||
|
||||
ipcMain.on('setBounds', function (e, bounds, maximize) {
|
||||
setBounds(bounds, maximize)
|
||||
})
|
||||
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
|
||||
ipc.on('openFiles', () => dialog.openFiles())
|
||||
|
||||
ipcMain.on('setAspectRatio', function (e, aspectRatio) {
|
||||
setAspectRatio(aspectRatio)
|
||||
})
|
||||
/**
|
||||
* Dock
|
||||
*/
|
||||
|
||||
ipcMain.on('setBadge', function (e, text) {
|
||||
setBadge(text)
|
||||
})
|
||||
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
|
||||
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
|
||||
|
||||
ipcMain.on('setProgress', function (e, progress) {
|
||||
setProgress(progress)
|
||||
})
|
||||
/**
|
||||
* Events
|
||||
*/
|
||||
|
||||
ipcMain.on('toggleFullScreen', function (e, flag) {
|
||||
menu.toggleFullScreen(flag)
|
||||
})
|
||||
|
||||
ipcMain.on('setTitle', function (e, title) {
|
||||
windows.main.setTitle(title)
|
||||
})
|
||||
|
||||
ipcMain.on('openItem', function (e, path) {
|
||||
log('open item: ' + path)
|
||||
electron.shell.openItem(path)
|
||||
})
|
||||
|
||||
ipcMain.on('showItemInFolder', function (e, path) {
|
||||
log('show item in folder: ' + path)
|
||||
electron.shell.showItemInFolder(path)
|
||||
})
|
||||
|
||||
ipcMain.on('blockPowerSave', blockPowerSave)
|
||||
|
||||
ipcMain.on('unblockPowerSave', unblockPowerSave)
|
||||
|
||||
ipcMain.on('onPlayerOpen', function () {
|
||||
ipc.on('onPlayerOpen', function () {
|
||||
menu.onPlayerOpen()
|
||||
shortcuts.onPlayerOpen()
|
||||
})
|
||||
|
||||
ipcMain.on('onPlayerClose', function () {
|
||||
ipc.on('onPlayerClose', function () {
|
||||
menu.onPlayerClose()
|
||||
shortcuts.onPlayerOpen()
|
||||
})
|
||||
|
||||
ipcMain.on('focusWindow', function (e, windowName) {
|
||||
windows.focusWindow(windows[windowName])
|
||||
})
|
||||
/**
|
||||
* Power Save Blocker
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
ipc.on('blockPowerSave', () => powerSaveBlocker.start())
|
||||
ipc.on('unblockPowerSave', () => powerSaveBlocker.stop())
|
||||
|
||||
ipcMain.on('checkForVLC', function (e) {
|
||||
/**
|
||||
* Shell
|
||||
*/
|
||||
|
||||
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||
|
||||
/**
|
||||
* Windows: Main
|
||||
*/
|
||||
|
||||
var main = windows.main
|
||||
|
||||
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
|
||||
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
|
||||
ipc.on('setProgress', (e, ...args) => main.setProgress(...args))
|
||||
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
||||
ipc.on('show', () => main.show())
|
||||
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
||||
|
||||
/**
|
||||
* VLC
|
||||
* TODO: Move most of this code to vlc.js
|
||||
*/
|
||||
|
||||
ipc.on('checkForVLC', function (e) {
|
||||
vlc.checkForVLC(function (isInstalled) {
|
||||
windows.main.send('checkForVLC', isInstalled)
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('vlcPlay', function (e, url) {
|
||||
var args = ['--play-and-exit', '--quiet', url]
|
||||
console.log('Running vlc ' + args.join(' '))
|
||||
ipc.on('vlcPlay', function (e, url) {
|
||||
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url]
|
||||
log('Running vlc ' + args.join(' '))
|
||||
|
||||
vlc.spawn(args, function (err, proc) {
|
||||
if (err) windows.main.send('dispatch', 'vlcNotFound')
|
||||
if (err) return windows.main.dispatch('vlcNotFound')
|
||||
vlcProcess = proc
|
||||
|
||||
// If it works, close the modal after a second
|
||||
var closeModalTimeout = setTimeout(() =>
|
||||
windows.main.send('dispatch', 'exitModal'), 1000)
|
||||
windows.main.dispatch('exitModal'), 1000)
|
||||
|
||||
vlcProcess.on('close', function (code) {
|
||||
clearTimeout(closeModalTimeout)
|
||||
if (!vlcProcess) return // Killed
|
||||
console.log('VLC exited with code ', code)
|
||||
log('VLC exited with code ', code)
|
||||
if (code === 0) {
|
||||
windows.main.send('dispatch', 'backToList')
|
||||
windows.main.dispatch('backToList')
|
||||
} else {
|
||||
windows.main.send('dispatch', 'vlcNotFound')
|
||||
windows.main.dispatch('vlcNotFound')
|
||||
}
|
||||
vlcProcess = null
|
||||
})
|
||||
|
||||
vlcProcess.on('error', function (e) {
|
||||
console.log('VLC error', e)
|
||||
log('VLC error', e)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('vlcQuit', function () {
|
||||
ipc.on('vlcQuit', function () {
|
||||
if (!vlcProcess) return
|
||||
console.log('Killing VLC, pid ' + vlcProcess.pid)
|
||||
log('Killing VLC, pid ' + vlcProcess.pid)
|
||||
vlcProcess.kill('SIGKILL') // kill -9
|
||||
vlcProcess = null
|
||||
})
|
||||
|
||||
// Capture all events
|
||||
var oldEmit = ipcMain.emit
|
||||
ipcMain.emit = function (name, e, ...args) {
|
||||
var oldEmit = ipc.emit
|
||||
ipc.emit = function (name, e, ...args) {
|
||||
// Relay messages between the main window and the WebTorrent hidden window
|
||||
if (name.startsWith('wt-') && !app.isQuitting) {
|
||||
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
|
||||
@@ -168,82 +168,6 @@ function init () {
|
||||
}
|
||||
|
||||
// Emit all other events normally
|
||||
oldEmit.call(ipcMain, name, e, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
function setBounds (bounds, maximize) {
|
||||
// Do nothing in fullscreen
|
||||
if (!windows.main || windows.main.isFullScreen()) {
|
||||
log('setBounds: not setting bounds because we\'re in full screen')
|
||||
return
|
||||
}
|
||||
|
||||
// Maximize or minimize, if the second argument is present
|
||||
var willBeMaximized
|
||||
if (maximize === true) {
|
||||
if (!windows.main.isMaximized()) {
|
||||
log('setBounds: maximizing')
|
||||
windows.main.maximize()
|
||||
}
|
||||
willBeMaximized = true
|
||||
} else if (maximize === false) {
|
||||
if (windows.main.isMaximized()) {
|
||||
log('setBounds: unmaximizing')
|
||||
windows.main.unmaximize()
|
||||
}
|
||||
willBeMaximized = false
|
||||
} else {
|
||||
willBeMaximized = windows.main.isMaximized()
|
||||
}
|
||||
|
||||
// Assuming we're not maximized or maximizing, set the window size
|
||||
if (!willBeMaximized) {
|
||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
||||
if (bounds.x === null && bounds.y === null) {
|
||||
// X and Y not specified? By default, center on current screen
|
||||
var scr = electron.screen.getDisplayMatching(windows.main.getBounds())
|
||||
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||
}
|
||||
windows.main.setBounds(bounds, true)
|
||||
} else {
|
||||
log('setBounds: not setting bounds because of window maximization')
|
||||
}
|
||||
}
|
||||
|
||||
function setAspectRatio (aspectRatio) {
|
||||
log('setAspectRatio %o', aspectRatio)
|
||||
if (windows.main) {
|
||||
windows.main.setAspectRatio(aspectRatio)
|
||||
}
|
||||
}
|
||||
|
||||
// Display string in dock badging area (OS X)
|
||||
function setBadge (text) {
|
||||
log('setBadge %s', text)
|
||||
if (app.dock) {
|
||||
app.dock.setBadge(String(text))
|
||||
}
|
||||
}
|
||||
|
||||
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
|
||||
function setProgress (progress) {
|
||||
log('setProgress %s', progress)
|
||||
if (windows.main) {
|
||||
windows.main.setProgressBar(progress)
|
||||
}
|
||||
}
|
||||
|
||||
function blockPowerSave () {
|
||||
powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||
log('blockPowerSave %d', powerSaveBlockerId)
|
||||
}
|
||||
|
||||
function unblockPowerSave () {
|
||||
if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) {
|
||||
electron.powerSaveBlocker.stop(powerSaveBlockerId)
|
||||
log('unblockPowerSave %d', powerSaveBlockerId)
|
||||
oldEmit.call(ipc, name, e, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ module.exports.error = error
|
||||
*/
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var windows = require('./windows')
|
||||
|
||||
var app = electron.app
|
||||
@@ -18,7 +17,7 @@ function log (...args) {
|
||||
if (app.ipcReady) {
|
||||
windows.main.send('log', ...args)
|
||||
} else {
|
||||
app.on('ipcReady', () => windows.main.send('log', ...args))
|
||||
app.once('ipcReady', () => windows.main.send('log', ...args))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +25,6 @@ function error (...args) {
|
||||
if (app.ipcReady) {
|
||||
windows.main.send('error', ...args)
|
||||
} else {
|
||||
app.on('ipcReady', () => windows.main.send('error', ...args))
|
||||
app.once('ipcReady', () => windows.main.send('error', ...args))
|
||||
}
|
||||
}
|
||||
|
||||
297
main/menu.js
297
main/menu.js
@@ -2,15 +2,10 @@ module.exports = {
|
||||
init,
|
||||
onPlayerClose,
|
||||
onPlayerOpen,
|
||||
onToggleAlwaysOnTop,
|
||||
onToggleFullScreen,
|
||||
onWindowHide,
|
||||
onWindowShow,
|
||||
|
||||
// TODO: move these out of menu.js -- they don't belong here
|
||||
showOpenSeedFiles,
|
||||
showOpenTorrentAddress,
|
||||
showOpenTorrentFile,
|
||||
toggleFullScreen
|
||||
onWindowBlur,
|
||||
onWindowFocus
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
@@ -18,170 +13,67 @@ var electron = require('electron')
|
||||
var app = electron.app
|
||||
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
var dialog = require('./dialog')
|
||||
var shell = require('./shell')
|
||||
var windows = require('./windows')
|
||||
|
||||
var appMenu
|
||||
var menu
|
||||
|
||||
function init () {
|
||||
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
|
||||
electron.Menu.setApplicationMenu(appMenu)
|
||||
|
||||
if (app.dock) {
|
||||
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
|
||||
app.dock.setMenu(dockMenu)
|
||||
}
|
||||
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
electron.Menu.setApplicationMenu(menu)
|
||||
}
|
||||
|
||||
function toggleFullScreen (flag) {
|
||||
log('toggleFullScreen %s', flag)
|
||||
if (windows.main && windows.main.isVisible()) {
|
||||
flag = flag != null ? flag : !windows.main.isFullScreen()
|
||||
if (flag) {
|
||||
// Allows the window to use the full screen in fullscreen mode (OS X).
|
||||
windows.main.setAspectRatio(0)
|
||||
}
|
||||
windows.main.setFullScreen(flag)
|
||||
}
|
||||
function onPlayerClose () {
|
||||
getMenuItem('Play/Pause').enabled = false
|
||||
getMenuItem('Increase Volume').enabled = false
|
||||
getMenuItem('Decrease Volume').enabled = false
|
||||
getMenuItem('Step Forward').enabled = false
|
||||
getMenuItem('Step Backward').enabled = false
|
||||
getMenuItem('Increase Speed').enabled = false
|
||||
getMenuItem('Decrease Speed').enabled = false
|
||||
getMenuItem('Add Subtitles File...').enabled = false
|
||||
}
|
||||
|
||||
// Sets whether the window should always show on top of other windows
|
||||
function toggleFloatOnTop (flag) {
|
||||
log('toggleFloatOnTop %s', flag)
|
||||
if (windows.main) {
|
||||
flag = flag != null ? flag : !windows.main.isAlwaysOnTop()
|
||||
windows.main.setAlwaysOnTop(flag)
|
||||
getMenuItem('Float on Top').checked = flag
|
||||
}
|
||||
function onPlayerOpen () {
|
||||
getMenuItem('Play/Pause').enabled = true
|
||||
getMenuItem('Increase Volume').enabled = true
|
||||
getMenuItem('Decrease Volume').enabled = true
|
||||
getMenuItem('Step Forward').enabled = true
|
||||
getMenuItem('Step Backward').enabled = true
|
||||
getMenuItem('Increase Speed').enabled = true
|
||||
getMenuItem('Decrease Speed').enabled = true
|
||||
getMenuItem('Add Subtitles File...').enabled = true
|
||||
}
|
||||
|
||||
function toggleDevTools () {
|
||||
log('toggleDevTools')
|
||||
if (windows.main) {
|
||||
windows.main.toggleDevTools()
|
||||
}
|
||||
function onToggleAlwaysOnTop (flag) {
|
||||
getMenuItem('Float on Top').checked = flag
|
||||
}
|
||||
|
||||
function showWebTorrentWindow () {
|
||||
log('showWebTorrentWindow')
|
||||
windows.webtorrent.show()
|
||||
windows.webtorrent.webContents.openDevTools({ detach: true })
|
||||
function onToggleFullScreen (flag) {
|
||||
getMenuItem('Full Screen').checked = flag
|
||||
}
|
||||
|
||||
function playPause () {
|
||||
if (windows.main) {
|
||||
windows.main.send('dispatch', 'playPause')
|
||||
}
|
||||
}
|
||||
|
||||
function increaseVolume () {
|
||||
if (windows.main) {
|
||||
windows.main.send('dispatch', 'changeVolume', 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
function decreaseVolume () {
|
||||
if (windows.main) {
|
||||
windows.main.send('dispatch', 'changeVolume', -0.1)
|
||||
}
|
||||
}
|
||||
|
||||
function openSubtitles () {
|
||||
if (windows.main) {
|
||||
windows.main.send('dispatch', 'openSubtitles')
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowShow () {
|
||||
log('onWindowShow')
|
||||
getMenuItem('Full Screen').enabled = true
|
||||
getMenuItem('Float on Top').enabled = true
|
||||
}
|
||||
|
||||
function onWindowHide () {
|
||||
log('onWindowHide')
|
||||
function onWindowBlur () {
|
||||
getMenuItem('Full Screen').enabled = false
|
||||
getMenuItem('Float on Top').enabled = false
|
||||
}
|
||||
|
||||
function onPlayerOpen () {
|
||||
log('onPlayerOpen')
|
||||
getMenuItem('Play/Pause').enabled = true
|
||||
getMenuItem('Increase Volume').enabled = true
|
||||
getMenuItem('Decrease Volume').enabled = true
|
||||
getMenuItem('Add Subtitles File...').enabled = true
|
||||
}
|
||||
|
||||
function onPlayerClose () {
|
||||
log('onPlayerClose')
|
||||
getMenuItem('Play/Pause').enabled = false
|
||||
getMenuItem('Increase Volume').enabled = false
|
||||
getMenuItem('Decrease Volume').enabled = false
|
||||
getMenuItem('Add Subtitles File...').enabled = false
|
||||
}
|
||||
|
||||
function onToggleFullScreen (isFullScreen) {
|
||||
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen()
|
||||
windows.main.setMenuBarVisibility(!isFullScreen)
|
||||
getMenuItem('Full Screen').checked = isFullScreen
|
||||
windows.main.send('fullscreenChanged', isFullScreen)
|
||||
function onWindowFocus () {
|
||||
getMenuItem('Full Screen').enabled = true
|
||||
getMenuItem('Float on Top').enabled = true
|
||||
}
|
||||
|
||||
function getMenuItem (label) {
|
||||
for (var i = 0; i < appMenu.items.length; i++) {
|
||||
var menuItem = appMenu.items[i].submenu.items.find(function (item) {
|
||||
for (var i = 0; i < menu.items.length; i++) {
|
||||
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||
return item.label === label
|
||||
})
|
||||
if (menuItem) return menuItem
|
||||
}
|
||||
}
|
||||
|
||||
// Prompts the user for a file, then creates a torrent. Only allows a single file
|
||||
// selection.
|
||||
function showOpenSeedFile () {
|
||||
electron.dialog.showOpenDialog({
|
||||
title: 'Select a file for the torrent file.',
|
||||
properties: [ 'openFile' ]
|
||||
}, function (selectedPaths) {
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
var selectedPath = selectedPaths[0]
|
||||
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
|
||||
})
|
||||
}
|
||||
|
||||
// Prompts the user for a file or directory, then creates a torrent. Only allows a single
|
||||
// selection. To create a multi-file torrent, the user must select a directory.
|
||||
function showOpenSeedFiles () {
|
||||
electron.dialog.showOpenDialog({
|
||||
title: 'Select a file or folder for the torrent file.',
|
||||
properties: [ 'openFile', 'openDirectory' ]
|
||||
}, function (selectedPaths) {
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
var selectedPath = selectedPaths[0]
|
||||
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
|
||||
})
|
||||
}
|
||||
|
||||
// Prompts the user to choose a torrent file, then adds it to the app
|
||||
function showOpenTorrentFile () {
|
||||
electron.dialog.showOpenDialog(windows.main, {
|
||||
title: 'Select a .torrent file to open.',
|
||||
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
||||
properties: [ 'openFile', 'multiSelections' ]
|
||||
}, function (selectedPaths) {
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
selectedPaths.forEach(function (selectedPath) {
|
||||
windows.main.send('dispatch', 'addTorrent', selectedPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Prompts the user for the URL of a torrent file, then downloads and adds it
|
||||
function showOpenTorrentAddress () {
|
||||
windows.main.send('showOpenTorrentAddress')
|
||||
}
|
||||
|
||||
function getAppMenuTemplate () {
|
||||
function getMenuTemplate () {
|
||||
var template = [
|
||||
{
|
||||
label: 'File',
|
||||
@@ -191,17 +83,17 @@ function getAppMenuTemplate () {
|
||||
? 'Create New Torrent...'
|
||||
: 'Create New Torrent from Folder...',
|
||||
accelerator: 'CmdOrCtrl+N',
|
||||
click: showOpenSeedFiles
|
||||
click: () => dialog.openSeedDirectory()
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent File...',
|
||||
accelerator: 'CmdOrCtrl+O',
|
||||
click: showOpenTorrentFile
|
||||
click: () => dialog.openTorrentFile()
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent Address...',
|
||||
accelerator: 'CmdOrCtrl+U',
|
||||
click: showOpenTorrentAddress
|
||||
click: () => dialog.openTorrentAddress()
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
@@ -237,6 +129,14 @@ function getAppMenuTemplate () {
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
role: 'selectall'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
click: () => windows.main.dispatch('preferences')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -249,12 +149,20 @@ function getAppMenuTemplate () {
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'Ctrl+Command+F'
|
||||
: 'F11',
|
||||
click: () => toggleFullScreen()
|
||||
click: () => windows.main.toggleFullScreen()
|
||||
},
|
||||
{
|
||||
label: 'Float on Top',
|
||||
type: 'checkbox',
|
||||
click: () => toggleFloatOnTop()
|
||||
click: () => windows.main.toggleAlwaysOnTop()
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Go Back',
|
||||
accelerator: 'Esc',
|
||||
click: () => windows.main.dispatch('escapeBack')
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
@@ -267,14 +175,14 @@ function getAppMenuTemplate () {
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'Alt+Command+I'
|
||||
: 'Ctrl+Shift+I',
|
||||
click: toggleDevTools
|
||||
click: () => windows.main.toggleDevTools()
|
||||
},
|
||||
{
|
||||
label: 'Show WebTorrent Process',
|
||||
accelerator: process.platform === 'darwin'
|
||||
? 'Alt+Command+P'
|
||||
: 'Ctrl+Shift+P',
|
||||
click: showWebTorrentWindow
|
||||
click: () => windows.webtorrent.toggleDevTools()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -285,8 +193,8 @@ function getAppMenuTemplate () {
|
||||
submenu: [
|
||||
{
|
||||
label: 'Play/Pause',
|
||||
accelerator: 'CmdOrCtrl+P',
|
||||
click: playPause,
|
||||
accelerator: 'Space',
|
||||
click: () => windows.main.dispatch('playPause'),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
@@ -295,13 +203,43 @@ function getAppMenuTemplate () {
|
||||
{
|
||||
label: 'Increase Volume',
|
||||
accelerator: 'CmdOrCtrl+Up',
|
||||
click: increaseVolume,
|
||||
click: () => windows.main.dispatch('changeVolume', 0.1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Decrease Volume',
|
||||
accelerator: 'CmdOrCtrl+Down',
|
||||
click: decreaseVolume,
|
||||
click: () => windows.main.dispatch('changeVolume', -0.1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Step Forward',
|
||||
accelerator: 'CmdOrCtrl+Alt+Right',
|
||||
click: () => windows.main.dispatch('skip', 1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Step Backward',
|
||||
accelerator: 'CmdOrCtrl+Alt+Left',
|
||||
click: () => windows.main.dispatch('skip', -1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Increase Speed',
|
||||
accelerator: 'CmdOrCtrl+=',
|
||||
click: () => windows.main.dispatch('changePlaybackRate', 1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Decrease Speed',
|
||||
accelerator: 'CmdOrCtrl+-',
|
||||
click: () => windows.main.dispatch('changePlaybackRate', -1),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
@@ -309,7 +247,7 @@ function getAppMenuTemplate () {
|
||||
},
|
||||
{
|
||||
label: 'Add Subtitles File...',
|
||||
click: openSubtitles,
|
||||
click: () => windows.main.dispatch('openSubtitles'),
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
@@ -320,18 +258,18 @@ function getAppMenuTemplate () {
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn more about ' + config.APP_NAME,
|
||||
click: () => electron.shell.openExternal(config.HOME_PAGE_URL)
|
||||
click: () => shell.openExternal(config.HOME_PAGE_URL)
|
||||
},
|
||||
{
|
||||
label: 'Contribute on GitHub',
|
||||
click: () => electron.shell.openExternal(config.GITHUB_URL)
|
||||
click: () => shell.openExternal(config.GITHUB_URL)
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Report an Issue...',
|
||||
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -349,6 +287,14 @@ function getAppMenuTemplate () {
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'Cmd+,',
|
||||
click: () => windows.main.dispatch('preferences')
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Services',
|
||||
role: 'services',
|
||||
@@ -403,12 +349,13 @@ function getAppMenuTemplate () {
|
||||
})
|
||||
}
|
||||
|
||||
// In Linux and Windows it is not possible to open both folders and files
|
||||
// On Windows and Linux, open dialogs do not support selecting both files and
|
||||
// folders and files, so add an extra menu item so there is one for each type.
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
// File menu (Windows, Linux)
|
||||
template[0].submenu.unshift({
|
||||
label: 'Create New Torrent from File...',
|
||||
click: showOpenSeedFile
|
||||
click: () => dialog.openSeedFile()
|
||||
})
|
||||
|
||||
// Help menu (Windows, Linux)
|
||||
@@ -418,12 +365,12 @@ function getAppMenuTemplate () {
|
||||
},
|
||||
{
|
||||
label: 'About ' + config.APP_NAME,
|
||||
click: windows.createAboutWindow
|
||||
click: () => windows.about.init()
|
||||
}
|
||||
)
|
||||
}
|
||||
// Add "File > Quit" menu item so Linux distros where the system tray icon is missing
|
||||
// will have a way to quit the app.
|
||||
// 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({
|
||||
@@ -434,23 +381,3 @@ function getAppMenuTemplate () {
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
function getDockMenuTemplate () {
|
||||
return [
|
||||
{
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
30
main/power-save-blocker.js
Normal file
30
main/power-save-blocker.js
Normal file
@@ -0,0 +1,30 @@
|
||||
module.exports = {
|
||||
start,
|
||||
stop
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
var log = require('./log')
|
||||
|
||||
var blockId = 0
|
||||
|
||||
/**
|
||||
* Block the system from entering low-power (sleep) mode or turning off the
|
||||
* display.
|
||||
*/
|
||||
function start () {
|
||||
stop() // Stop the previous power saver block, if one exists.
|
||||
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||
log(`powerSaveBlocker.start: ${blockId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop blocking the system from entering low-power mode.
|
||||
*/
|
||||
function stop () {
|
||||
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
||||
return
|
||||
}
|
||||
electron.powerSaveBlocker.stop(blockId)
|
||||
log(`powerSaveBlocker.stop: ${blockId}`)
|
||||
}
|
||||
32
main/shell.js
Normal file
32
main/shell.js
Normal file
@@ -0,0 +1,32 @@
|
||||
module.exports = {
|
||||
openExternal,
|
||||
openItem,
|
||||
showItemInFolder
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
var log = require('./log')
|
||||
|
||||
/**
|
||||
* Open the given external protocol URL in the desktop’s default manner.
|
||||
*/
|
||||
function openExternal (url) {
|
||||
log(`openExternal: ${url}`)
|
||||
electron.shell.openExternal(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given file in the desktop’s default manner.
|
||||
*/
|
||||
function openItem (path) {
|
||||
log(`openItem: ${path}`)
|
||||
electron.shell.openItem(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the given file in a file manager. If possible, select the file.
|
||||
*/
|
||||
function showItemInFolder (path) {
|
||||
log(`showItemInFolder: ${path}`)
|
||||
electron.shell.showItemInFolder(path)
|
||||
}
|
||||
@@ -1,34 +1,20 @@
|
||||
module.exports = {
|
||||
init,
|
||||
onPlayerClose,
|
||||
onPlayerOpen
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var menu = require('./menu')
|
||||
var windows = require('./windows')
|
||||
|
||||
function init () {
|
||||
var localShortcut = require('electron-localshortcut')
|
||||
|
||||
// Alternate shortcuts. Most shortcuts are registered in menu,js, but Electron
|
||||
// does not support multiple shortcuts for a single menu item.
|
||||
localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen)
|
||||
localShortcut.register('Space', () => windows.main.send('dispatch', 'playPause'))
|
||||
|
||||
// Hidden shortcuts, i.e. not shown in the menu
|
||||
localShortcut.register('Esc', () => windows.main.send('dispatch', 'escapeBack'))
|
||||
}
|
||||
|
||||
function onPlayerOpen () {
|
||||
// Register special "media key" for play/pause, available on some keyboards
|
||||
// Register play/pause media key, available on some keyboards.
|
||||
electron.globalShortcut.register(
|
||||
'MediaPlayPause',
|
||||
() => windows.main.send('dispatch', 'playPause')
|
||||
() => windows.main.dispatch('playPause')
|
||||
)
|
||||
}
|
||||
|
||||
function onPlayerClose () {
|
||||
// Return the media key to the OS, so other apps can use it.
|
||||
electron.globalShortcut.unregister('MediaPlayPause')
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ var app = electron.app
|
||||
|
||||
var handlers = require('./handlers')
|
||||
|
||||
var exeName = path.basename(process.execPath)
|
||||
var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||
var EXE_NAME = path.basename(process.execPath)
|
||||
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||
|
||||
function handleEvent (cmd) {
|
||||
if (cmd === '--squirrel-install') {
|
||||
@@ -61,15 +61,17 @@ function handleEvent (cmd) {
|
||||
}
|
||||
|
||||
if (cmd === '--squirrel-firstrun') {
|
||||
// This is called on the app's first run. Do not quit, allow startup to continue.
|
||||
// App is running for the first time. Do not quit, allow startup to continue.
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Spawn a command and invoke the callback when it completes with an error and the output
|
||||
// from standard out.
|
||||
/**
|
||||
* Spawn a command and invoke the callback when it completes with an error and
|
||||
* the output from standard out.
|
||||
*/
|
||||
function spawn (command, args, cb) {
|
||||
var stdout = ''
|
||||
|
||||
@@ -99,24 +101,31 @@ function spawn (command, args, cb) {
|
||||
})
|
||||
}
|
||||
|
||||
// Spawn Squirrel's Update.exe with the given arguments and invoke the callback when the
|
||||
// command completes.
|
||||
/**
|
||||
* Spawn the Squirrel `Update.exe` command with the given arguments and invoke
|
||||
* the callback when the command completes.
|
||||
*/
|
||||
function spawnUpdate (args, cb) {
|
||||
spawn(updateDotExe, args, cb)
|
||||
spawn(UPDATE_EXE, args, cb)
|
||||
}
|
||||
|
||||
// Create desktop/start menu shortcuts using the Squirrel Update.exe command line API
|
||||
/**
|
||||
* Create desktop and start menu shortcuts using the Squirrel `Update.exe`
|
||||
* command.
|
||||
*/
|
||||
function createShortcuts (cb) {
|
||||
spawnUpdate(['--createShortcut', exeName], cb)
|
||||
spawnUpdate(['--createShortcut', EXE_NAME], cb)
|
||||
}
|
||||
|
||||
// Update desktop/start menu shortcuts using the Squirrel Update.exe command line API
|
||||
/**
|
||||
* Update desktop and start menu shortcuts using the Squirrel `Update.exe`
|
||||
* command.
|
||||
*/
|
||||
function updateShortcuts (cb) {
|
||||
var homeDir = os.homedir()
|
||||
if (homeDir) {
|
||||
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
||||
// Check if the desktop shortcut has been previously deleted and and keep it deleted
|
||||
// if it was
|
||||
// If the desktop shortcut was deleted by the user, then keep it deleted.
|
||||
fs.access(desktopShortcutPath, function (err) {
|
||||
var desktopShortcutExists = !err
|
||||
createShortcuts(function () {
|
||||
@@ -133,7 +142,10 @@ function updateShortcuts (cb) {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API
|
||||
/**
|
||||
* Remove desktop and start menu shortcuts using the Squirrel `Update.exe`
|
||||
* command.
|
||||
*/
|
||||
function removeShortcuts (cb) {
|
||||
spawnUpdate(['--removeShortcut', exeName], cb)
|
||||
spawnUpdate(['--removeShortcut', EXE_NAME], cb)
|
||||
}
|
||||
|
||||
118
main/tray.js
118
main/tray.js
@@ -1,51 +1,62 @@
|
||||
module.exports = {
|
||||
hasTray,
|
||||
init,
|
||||
hasTray
|
||||
onWindowBlur,
|
||||
onWindowFocus
|
||||
}
|
||||
|
||||
var cp = require('child_process')
|
||||
var path = require('path')
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
|
||||
var config = require('../config')
|
||||
var windows = require('./windows')
|
||||
|
||||
var trayIcon
|
||||
var tray
|
||||
|
||||
function init () {
|
||||
// OS X has no tray icon
|
||||
if (process.platform === 'darwin') return
|
||||
|
||||
// On Linux, asynchronously check for libappindicator1
|
||||
if (process.platform === 'linux') {
|
||||
checkLinuxTraySupport(function (supportsTray) {
|
||||
if (supportsTray) createTrayIcon()
|
||||
})
|
||||
initLinux()
|
||||
}
|
||||
|
||||
// Windows always supports minimize-to-tray
|
||||
if (process.platform === 'win32') createTrayIcon()
|
||||
if (process.platform === 'win32') {
|
||||
initWin32()
|
||||
}
|
||||
// OS X apps generally do not have menu bar icons
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there a tray icon is active.
|
||||
*/
|
||||
function hasTray () {
|
||||
return !!trayIcon
|
||||
return !!tray
|
||||
}
|
||||
|
||||
function createTrayIcon () {
|
||||
trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
|
||||
|
||||
// On Windows, left click to open the app, right click for context menu
|
||||
// On Linux, any click (right or left) opens the context menu
|
||||
trayIcon.on('click', showApp)
|
||||
|
||||
// Show the tray context menu, and keep the available commands up to date
|
||||
function onWindowBlur () {
|
||||
if (!tray) return
|
||||
updateTrayMenu()
|
||||
windows.main.on('show', updateTrayMenu)
|
||||
windows.main.on('hide', updateTrayMenu)
|
||||
}
|
||||
|
||||
function onWindowFocus () {
|
||||
if (!tray) return
|
||||
updateTrayMenu()
|
||||
}
|
||||
|
||||
function initLinux () {
|
||||
checkLinuxTraySupport(function (supportsTray) {
|
||||
if (supportsTray) createTray()
|
||||
})
|
||||
}
|
||||
|
||||
function initWin32 () {
|
||||
createTray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for libappindicator1 support before creating tray icon
|
||||
*/
|
||||
function checkLinuxTraySupport (cb) {
|
||||
var cp = require('child_process')
|
||||
|
||||
// Check that we're on Ubuntu (or another debian system) and that we have
|
||||
// libappindicator1. If WebTorrent was installed from the deb file, we should
|
||||
// always have it. If it was installed from the zip file, we might not.
|
||||
@@ -57,25 +68,48 @@ function checkLinuxTraySupport (cb) {
|
||||
})
|
||||
}
|
||||
|
||||
function createTray () {
|
||||
tray = new electron.Tray(getIconPath())
|
||||
|
||||
// On Windows, left click opens the app, right click opens the context menu.
|
||||
// On Linux, any click (left or right) opens the context menu.
|
||||
tray.on('click', () => windows.main.show())
|
||||
|
||||
// Show the tray context menu, and keep the available commands up to date
|
||||
updateTrayMenu()
|
||||
}
|
||||
|
||||
function updateTrayMenu () {
|
||||
var showHideMenuItem
|
||||
if (windows.main.isVisible()) {
|
||||
showHideMenuItem = { label: 'Hide to tray', click: hideApp }
|
||||
} else {
|
||||
showHideMenuItem = { label: 'Show', click: showApp }
|
||||
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
tray.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
function getMenuTemplate () {
|
||||
return [
|
||||
getToggleItem(),
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => app.quit()
|
||||
}
|
||||
]
|
||||
|
||||
function getToggleItem () {
|
||||
if (windows.main.win.isVisible()) {
|
||||
return {
|
||||
label: 'Hide to tray',
|
||||
click: () => windows.main.hide()
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
label: 'Show WebTorrent',
|
||||
click: () => windows.main.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
var contextMenu = electron.Menu.buildFromTemplate([
|
||||
showHideMenuItem,
|
||||
{ label: 'Quit', click: () => app.quit() }
|
||||
])
|
||||
trayIcon.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
function showApp () {
|
||||
windows.main.show()
|
||||
}
|
||||
|
||||
function hideApp () {
|
||||
windows.main.hide()
|
||||
windows.main.send('dispatch', 'backToList')
|
||||
function getIconPath () {
|
||||
return process.platform === 'win32'
|
||||
? config.APP_ICON + '.ico'
|
||||
: config.APP_ICON + '.png'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ 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()
|
||||
@@ -17,27 +21,27 @@ function init () {
|
||||
}
|
||||
}
|
||||
|
||||
// The Electron auto-updater does not support Linux yet, so manually check for updates and
|
||||
// `show the user a modal notification.
|
||||
// The Electron auto-updater does not support Linux yet, so manually check for
|
||||
// updates and show the user a modal notification.
|
||||
function initLinux () {
|
||||
get.concat(config.AUTO_UPDATE_URL, onResponse)
|
||||
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 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.dispatch('updateAvailable', data.version)
|
||||
} else if (res.statusCode === 204) {
|
||||
// No update available
|
||||
} else {
|
||||
// Unexpected status code
|
||||
log(`Update error: Unexpected status code: ${res.statusCode}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +71,6 @@ function initDarwinWin32 () {
|
||||
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
|
||||
)
|
||||
|
||||
electron.autoUpdater.setFeedURL(config.AUTO_UPDATE_URL)
|
||||
electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
|
||||
electron.autoUpdater.checkForUpdates()
|
||||
}
|
||||
|
||||
139
main/windows.js
139
main/windows.js
@@ -1,139 +0,0 @@
|
||||
var windows = module.exports = {
|
||||
about: null,
|
||||
main: null,
|
||||
createAboutWindow,
|
||||
createWebTorrentHiddenWindow,
|
||||
createMainWindow,
|
||||
focusWindow
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
|
||||
var config = require('../config')
|
||||
var menu = require('./menu')
|
||||
var tray = require('./tray')
|
||||
|
||||
function createAboutWindow () {
|
||||
if (windows.about) {
|
||||
return focusWindow(windows.about)
|
||||
}
|
||||
var win = windows.about = new electron.BrowserWindow({
|
||||
backgroundColor: '#ECECEC',
|
||||
show: false,
|
||||
center: true,
|
||||
resizable: false,
|
||||
icon: config.APP_ICON + '.png',
|
||||
title: process.platform !== 'darwin'
|
||||
? 'About ' + config.APP_WINDOW_TITLE
|
||||
: '',
|
||||
useContentSize: true, // Specify web page size without OS chrome
|
||||
width: 300,
|
||||
height: 170,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
fullscreen: false,
|
||||
skipTaskbar: true
|
||||
})
|
||||
win.loadURL(config.WINDOW_ABOUT)
|
||||
|
||||
// No window menu
|
||||
win.setMenu(null)
|
||||
|
||||
win.webContents.on('did-finish-load', function () {
|
||||
win.show()
|
||||
})
|
||||
|
||||
win.once('closed', function () {
|
||||
windows.about = null
|
||||
})
|
||||
}
|
||||
|
||||
function createWebTorrentHiddenWindow () {
|
||||
var win = windows.webtorrent = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
show: false,
|
||||
center: true,
|
||||
title: 'webtorrent-hidden-window',
|
||||
useContentSize: true,
|
||||
width: 150,
|
||||
height: 150,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
resizable: false,
|
||||
fullscreenable: false,
|
||||
fullscreen: false,
|
||||
skipTaskbar: true
|
||||
})
|
||||
win.loadURL(config.WINDOW_WEBTORRENT)
|
||||
|
||||
// Prevent killing the WebTorrent process
|
||||
win.on('close', function (e) {
|
||||
if (!app.isQuitting) {
|
||||
e.preventDefault()
|
||||
win.hide()
|
||||
}
|
||||
})
|
||||
|
||||
win.once('closed', function () {
|
||||
windows.webtorrent = null
|
||||
})
|
||||
}
|
||||
|
||||
var HEADER_HEIGHT = 37
|
||||
var TORRENT_HEIGHT = 100
|
||||
|
||||
function createMainWindow () {
|
||||
if (windows.main) {
|
||||
return focusWindow(windows.main)
|
||||
}
|
||||
var win = windows.main = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
darkTheme: true, // Forces dark theme (GTK+3)
|
||||
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
|
||||
minWidth: config.WINDOW_MIN_WIDTH,
|
||||
minHeight: config.WINDOW_MIN_HEIGHT,
|
||||
show: false, // Hide window until renderer sends 'ipcReady' event
|
||||
title: config.APP_WINDOW_TITLE,
|
||||
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
||||
useContentSize: true, // Specify web page size without OS chrome
|
||||
width: 500,
|
||||
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
||||
})
|
||||
win.loadURL(config.WINDOW_MAIN)
|
||||
if (process.platform === 'darwin') {
|
||||
win.setSheetOffset(HEADER_HEIGHT)
|
||||
}
|
||||
|
||||
win.webContents.on('dom-ready', function () {
|
||||
menu.onToggleFullScreen()
|
||||
})
|
||||
|
||||
win.on('blur', menu.onWindowHide)
|
||||
win.on('focus', menu.onWindowShow)
|
||||
|
||||
win.on('enter-full-screen', () => menu.onToggleFullScreen(true))
|
||||
win.on('leave-full-screen', () => menu.onToggleFullScreen(false))
|
||||
|
||||
win.on('close', function (e) {
|
||||
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
||||
app.quit()
|
||||
} else if (!app.isQuitting) {
|
||||
e.preventDefault()
|
||||
win.hide()
|
||||
win.send('dispatch', 'backToList')
|
||||
}
|
||||
})
|
||||
|
||||
win.once('closed', function () {
|
||||
windows.main = null
|
||||
})
|
||||
}
|
||||
|
||||
function focusWindow (win) {
|
||||
if (win.isMinimized()) {
|
||||
win.restore()
|
||||
}
|
||||
win.show() // shows and gives focus
|
||||
}
|
||||
47
main/windows/about.js
Normal file
47
main/windows/about.js
Normal file
@@ -0,0 +1,47 @@
|
||||
var about = module.exports = {
|
||||
init,
|
||||
win: null
|
||||
}
|
||||
|
||||
var config = require('../../config')
|
||||
var electron = require('electron')
|
||||
|
||||
function init () {
|
||||
if (about.win) {
|
||||
return about.win.show()
|
||||
}
|
||||
|
||||
var win = about.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#ECECEC',
|
||||
center: true,
|
||||
fullscreen: false,
|
||||
height: 170,
|
||||
icon: getIconPath(),
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
resizable: false,
|
||||
show: false,
|
||||
skipTaskbar: true,
|
||||
useContentSize: true,
|
||||
width: 300
|
||||
})
|
||||
|
||||
win.loadURL(config.WINDOW_ABOUT)
|
||||
|
||||
// No menu on the About window
|
||||
win.setMenu(null)
|
||||
|
||||
win.webContents.on('did-finish-load', function () {
|
||||
win.show()
|
||||
})
|
||||
|
||||
win.once('closed', function () {
|
||||
about.win = null
|
||||
})
|
||||
}
|
||||
|
||||
function getIconPath () {
|
||||
return process.platform === 'win32'
|
||||
? config.APP_ICON + '.ico'
|
||||
: config.APP_ICON + '.png'
|
||||
}
|
||||
3
main/windows/index.js
Normal file
3
main/windows/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
exports.about = require('./about')
|
||||
exports.main = require('./main')
|
||||
exports.webtorrent = require('./webtorrent')
|
||||
214
main/windows/main.js
Normal file
214
main/windows/main.js
Normal file
@@ -0,0 +1,214 @@
|
||||
var main = module.exports = {
|
||||
dispatch,
|
||||
hide,
|
||||
init,
|
||||
send,
|
||||
setAspectRatio,
|
||||
setBounds,
|
||||
setProgress,
|
||||
setTitle,
|
||||
show,
|
||||
toggleAlwaysOnTop,
|
||||
toggleDevTools,
|
||||
toggleFullScreen,
|
||||
win: null
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
|
||||
var config = require('../../config')
|
||||
var log = require('../log')
|
||||
var menu = require('../menu')
|
||||
var tray = require('../tray')
|
||||
|
||||
var HEADER_HEIGHT = 37
|
||||
var TORRENT_HEIGHT = 100
|
||||
|
||||
function init () {
|
||||
if (main.win) {
|
||||
return main.win.show()
|
||||
}
|
||||
var win = main.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
darkTheme: true, // Forces dark theme (GTK+3)
|
||||
icon: getIconPath(), // Window icon (Windows, Linux)
|
||||
minWidth: config.WINDOW_MIN_WIDTH,
|
||||
minHeight: config.WINDOW_MIN_HEIGHT,
|
||||
title: config.APP_WINDOW_TITLE,
|
||||
titleBarStyle: 'hidden-inset', // Hide title bar (OS X)
|
||||
useContentSize: true, // Specify web page size without OS chrome
|
||||
width: 500,
|
||||
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
||||
})
|
||||
|
||||
win.loadURL(config.WINDOW_MAIN)
|
||||
|
||||
if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT)
|
||||
|
||||
win.webContents.on('dom-ready', function () {
|
||||
menu.onToggleFullScreen(main.win.isFullScreen())
|
||||
})
|
||||
|
||||
win.on('blur', function () {
|
||||
menu.onWindowBlur()
|
||||
tray.onWindowBlur()
|
||||
})
|
||||
|
||||
win.on('focus', function () {
|
||||
menu.onWindowFocus()
|
||||
tray.onWindowFocus()
|
||||
})
|
||||
|
||||
win.on('enter-full-screen', function () {
|
||||
menu.onToggleFullScreen(true)
|
||||
send('fullscreenChanged', true)
|
||||
win.setMenuBarVisibility(false)
|
||||
})
|
||||
|
||||
win.on('leave-full-screen', function () {
|
||||
menu.onToggleFullScreen(false)
|
||||
send('fullscreenChanged', false)
|
||||
win.setMenuBarVisibility(true)
|
||||
})
|
||||
|
||||
win.on('close', function (e) {
|
||||
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
||||
app.quit()
|
||||
} else if (!app.isQuitting) {
|
||||
e.preventDefault()
|
||||
win.hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function dispatch (...args) {
|
||||
send('dispatch', ...args)
|
||||
}
|
||||
|
||||
function hide () {
|
||||
if (!main.win) return
|
||||
main.win.send('dispatch', 'backToList')
|
||||
main.win.hide()
|
||||
}
|
||||
|
||||
function send (...args) {
|
||||
if (!main.win) return
|
||||
main.win.send(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce window aspect ratio. Remove with 0. (OS X)
|
||||
*/
|
||||
function setAspectRatio (aspectRatio) {
|
||||
if (!main.win) return
|
||||
main.win.setAspectRatio(aspectRatio)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the size of the window.
|
||||
* TODO: Clean this up? Seems overly complicated.
|
||||
*/
|
||||
function setBounds (bounds, maximize) {
|
||||
// Do nothing in fullscreen
|
||||
if (!main.win || main.win.isFullScreen()) {
|
||||
log('setBounds: not setting bounds because we\'re in full screen')
|
||||
return
|
||||
}
|
||||
|
||||
// Maximize or minimize, if the second argument is present
|
||||
var willBeMaximized
|
||||
if (maximize === true) {
|
||||
if (!main.win.isMaximized()) {
|
||||
log('setBounds: maximizing')
|
||||
main.win.maximize()
|
||||
}
|
||||
willBeMaximized = true
|
||||
} else if (maximize === false) {
|
||||
if (main.win.isMaximized()) {
|
||||
log('setBounds: unmaximizing')
|
||||
main.win.unmaximize()
|
||||
}
|
||||
willBeMaximized = false
|
||||
} else {
|
||||
willBeMaximized = main.win.isMaximized()
|
||||
}
|
||||
|
||||
// Assuming we're not maximized or maximizing, set the window size
|
||||
if (!willBeMaximized) {
|
||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
||||
if (bounds.x === null && bounds.y === null) {
|
||||
// X and Y not specified? By default, center on current screen
|
||||
var scr = electron.screen.getDisplayMatching(main.win.getBounds())
|
||||
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||
}
|
||||
main.win.setBounds(bounds, true)
|
||||
} else {
|
||||
log('setBounds: not setting bounds because of window maximization')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set progress bar to [0, 1]. Indeterminate when > 1. Remove with < 0.
|
||||
*/
|
||||
function setProgress (progress) {
|
||||
if (!main.win) return
|
||||
main.win.setProgressBar(progress)
|
||||
}
|
||||
|
||||
function setTitle (title) {
|
||||
if (!main.win) return
|
||||
main.win.setTitle(title)
|
||||
}
|
||||
|
||||
function show () {
|
||||
if (!main.win) return
|
||||
main.win.show()
|
||||
}
|
||||
|
||||
// Sets whether the window should always show on top of other windows
|
||||
function toggleAlwaysOnTop (flag) {
|
||||
if (!main.win) return
|
||||
if (flag == null) {
|
||||
flag = !main.win.isAlwaysOnTop()
|
||||
}
|
||||
log(`toggleAlwaysOnTop ${flag}`)
|
||||
main.win.setAlwaysOnTop(flag)
|
||||
menu.onToggleAlwaysOnTop(flag)
|
||||
}
|
||||
|
||||
function toggleDevTools () {
|
||||
if (!main.win) return
|
||||
log('toggleDevTools')
|
||||
if (main.win.webContents.isDevToolsOpened()) {
|
||||
main.win.webContents.closeDevTools()
|
||||
} else {
|
||||
main.win.webContents.openDevTools({ detach: true })
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullScreen (flag) {
|
||||
if (!main.win || !main.win.isVisible()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (flag == null) flag = !main.win.isFullScreen()
|
||||
|
||||
log(`toggleFullScreen ${flag}`)
|
||||
|
||||
if (flag) {
|
||||
// Fullscreen and aspect ratio do not play well together. (OS X)
|
||||
main.win.setAspectRatio(0)
|
||||
}
|
||||
|
||||
main.win.setFullScreen(flag)
|
||||
}
|
||||
|
||||
function getIconPath () {
|
||||
return process.platform === 'win32'
|
||||
? config.APP_ICON + '.ico'
|
||||
: config.APP_ICON + '.png'
|
||||
}
|
||||
62
main/windows/webtorrent.js
Normal file
62
main/windows/webtorrent.js
Normal file
@@ -0,0 +1,62 @@
|
||||
var webtorrent = module.exports = {
|
||||
init,
|
||||
send,
|
||||
show,
|
||||
toggleDevTools,
|
||||
win: null
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var config = require('../../config')
|
||||
var log = require('../log')
|
||||
|
||||
function init () {
|
||||
var win = webtorrent.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
center: true,
|
||||
fullscreen: false,
|
||||
fullscreenable: false,
|
||||
height: 150,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
resizable: false,
|
||||
show: false,
|
||||
skipTaskbar: true,
|
||||
title: 'webtorrent-hidden-window',
|
||||
useContentSize: true,
|
||||
width: 150
|
||||
})
|
||||
|
||||
win.loadURL(config.WINDOW_WEBTORRENT)
|
||||
|
||||
// Prevent killing the WebTorrent process
|
||||
win.on('close', function (e) {
|
||||
if (electron.app.isQuitting) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
win.hide()
|
||||
})
|
||||
}
|
||||
|
||||
function show () {
|
||||
if (!webtorrent.win) return
|
||||
webtorrent.win.show()
|
||||
}
|
||||
|
||||
function send (...args) {
|
||||
if (!webtorrent.win) return
|
||||
webtorrent.win.send(...args)
|
||||
}
|
||||
|
||||
function toggleDevTools () {
|
||||
if (!webtorrent.win) return
|
||||
log('toggleDevTools')
|
||||
if (webtorrent.win.webContents.isDevToolsOpened()) {
|
||||
webtorrent.win.webContents.closeDevTools()
|
||||
webtorrent.win.hide()
|
||||
} else {
|
||||
webtorrent.win.webContents.openDevTools({ detach: true })
|
||||
}
|
||||
}
|
||||
21
package.json
21
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "webtorrent-desktop",
|
||||
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
||||
"version": "0.5.1",
|
||||
"version": "0.7.0",
|
||||
"author": {
|
||||
"name": "Feross Aboukhadijeh",
|
||||
"name": "WebTorrent, LLC",
|
||||
"email": "feross@feross.org",
|
||||
"url": "http://feross.org"
|
||||
"url": "https://webtorrent.io"
|
||||
},
|
||||
"bin": {
|
||||
"webtorrent-desktop": "./bin/cmd.js"
|
||||
@@ -14,18 +14,15 @@
|
||||
"url": "https://github.com/feross/webtorrent-desktop/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"airplay-js": "guerrerocarlos/node-airplay-js",
|
||||
"airplayer": "^2.0.0",
|
||||
"application-config": "^0.2.1",
|
||||
"async": "^2.0.0-rc.5",
|
||||
"bitfield": "^1.0.2",
|
||||
"chromecasts": "^1.8.0",
|
||||
"concat-stream": "^1.5.1",
|
||||
"create-torrent": "^3.24.5",
|
||||
"deep-equal": "^1.0.1",
|
||||
"dlnacasts": "^0.1.0",
|
||||
"drag-drop": "^2.11.0",
|
||||
"electron-localshortcut": "^0.6.0",
|
||||
"electron-prebuilt": "1.0.2",
|
||||
"electron-prebuilt": "1.2.1",
|
||||
"fs-extra": "^0.27.0",
|
||||
"hyperx": "^2.0.2",
|
||||
"iso-639-1": "^1.2.1",
|
||||
@@ -34,12 +31,16 @@
|
||||
"musicmetadata": "^2.0.2",
|
||||
"network-address": "^1.1.0",
|
||||
"prettier-bytes": "^1.0.1",
|
||||
"run-parallel": "^1.1.6",
|
||||
"semver": "^5.1.0",
|
||||
"simple-concat": "^1.0.0",
|
||||
"simple-get": "^2.0.0",
|
||||
"srt-to-vtt": "^1.1.1",
|
||||
"virtual-dom": "^2.1.1",
|
||||
"vlc-command": "^1.0.1",
|
||||
"webtorrent": "0.x",
|
||||
"winreg": "^1.2.0"
|
||||
"winreg": "^1.2.0",
|
||||
"zero-fill": "^2.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-zip": "^2.0.1",
|
||||
@@ -49,7 +50,7 @@
|
||||
"gh-release": "^2.0.3",
|
||||
"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",
|
||||
"rimraf": "^2.5.2",
|
||||
|
||||
@@ -8,10 +8,11 @@ module.exports = {
|
||||
play,
|
||||
pause,
|
||||
seek,
|
||||
setVolume
|
||||
setVolume,
|
||||
setRate
|
||||
}
|
||||
|
||||
var airplay = require('airplay-js')
|
||||
var airplayer = require('airplayer')()
|
||||
var chromecasts = require('chromecasts')()
|
||||
var dlnacasts = require('dlnacasts')()
|
||||
|
||||
@@ -40,10 +41,9 @@ function init (appState, callback) {
|
||||
state.devices.dlna = dlnaPlayer(player)
|
||||
})
|
||||
|
||||
var browser = airplay.createBrowser()
|
||||
browser.on('deviceOn', function (player) {
|
||||
airplayer.on('update', function (player) {
|
||||
state.devices.airplay = airplayPlayer(player)
|
||||
}).start()
|
||||
})
|
||||
}
|
||||
|
||||
// chromecast player implementation
|
||||
@@ -128,13 +128,31 @@ function chromecastPlayer (player) {
|
||||
|
||||
// airplay player implementation
|
||||
function airplayPlayer (player) {
|
||||
function addEvents () {
|
||||
player.on('event', function (event) {
|
||||
switch (event.state) {
|
||||
case 'loading':
|
||||
break
|
||||
case 'playing':
|
||||
state.playing.isPaused = false
|
||||
break
|
||||
case 'paused':
|
||||
state.playing.isPaused = true
|
||||
break
|
||||
case 'stopped':
|
||||
break
|
||||
}
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function open () {
|
||||
player.play(state.server.networkURL, 0, function (res) {
|
||||
if (res.statusCode !== 200) {
|
||||
player.play(state.server.networkURL, function (err, res) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to AirPlay.'
|
||||
message: 'Could not connect to AirPlay. ' + err.message
|
||||
})
|
||||
} else {
|
||||
state.playing.location = 'airplay'
|
||||
@@ -144,11 +162,11 @@ function airplayPlayer (player) {
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
player.rate(1, callback)
|
||||
player.resume(callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
player.rate(0, callback)
|
||||
player.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
@@ -156,13 +174,18 @@ function airplayPlayer (player) {
|
||||
}
|
||||
|
||||
function status () {
|
||||
player.status(function (status) {
|
||||
state.playing.isPaused = status.rate === 0
|
||||
state.playing.currentTime = status.position
|
||||
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume
|
||||
// According to docs is in [-30 - 0] (db) range
|
||||
// should be converted to [0 - 1] using (val / 30 + 1)
|
||||
update()
|
||||
player.playbackInfo(function (err, res, status) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to AirPlay. ' + err.message
|
||||
})
|
||||
} else {
|
||||
state.playing.isPaused = status.rate === 0
|
||||
state.playing.currentTime = status.position
|
||||
update()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -171,12 +194,13 @@ function airplayPlayer (player) {
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
// TODO remove line below once we can fetch the information in status update
|
||||
// AirPlay doesn't support volume
|
||||
// TODO: We should just disable the volume slider
|
||||
state.playing.volume = volume
|
||||
volume = (volume - 1) * 30
|
||||
player.volume(volume, callback)
|
||||
}
|
||||
|
||||
addEvents()
|
||||
|
||||
return {
|
||||
player: player,
|
||||
open: open,
|
||||
@@ -344,6 +368,22 @@ 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) {
|
||||
var device = getDevice()
|
||||
if (device) {
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
module.exports = {
|
||||
setDispatch,
|
||||
dispatch,
|
||||
dispatcher
|
||||
dispatcher,
|
||||
setDispatch
|
||||
}
|
||||
|
||||
// Memoize most of our event handlers, which are functions in the form
|
||||
// () => dispatch(<args>)
|
||||
// ... this prevents virtual-dom from updating every listener on every update()
|
||||
var _dispatchers = {}
|
||||
var _dispatch = () => {}
|
||||
var dispatchers = {}
|
||||
var _dispatch = function () {}
|
||||
|
||||
function setDispatch (dispatch) {
|
||||
_dispatch = dispatch
|
||||
}
|
||||
|
||||
// Get a _memoized event handler that calls dispatch()
|
||||
// All args must be JSON-able
|
||||
function dispatch (...args) {
|
||||
_dispatch(...args)
|
||||
}
|
||||
|
||||
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
|
||||
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler
|
||||
// function. This prevents virtual-dom from updating the listener functions on
|
||||
// each update().
|
||||
function dispatcher (...args) {
|
||||
var json = JSON.stringify(args)
|
||||
var handler = _dispatchers[json]
|
||||
var str = JSON.stringify(args)
|
||||
var handler = dispatchers[str]
|
||||
if (!handler) {
|
||||
handler = _dispatchers[json] = (e) => {
|
||||
// Don't click on whatever is below the button
|
||||
handler = dispatchers[str] = function (e) {
|
||||
// Do not propagate click to elements below the button
|
||||
e.stopPropagation()
|
||||
// Don't regisiter clicks on disabled buttons
|
||||
if (e.currentTarget.classList.contains('disabled')) return
|
||||
_dispatch.apply(null, args)
|
||||
|
||||
if (e.currentTarget.classList.contains('disabled')) {
|
||||
// Ignore clicks on disabled elements
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(...args)
|
||||
}
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
function dispatch (...args) {
|
||||
_dispatch.apply(null, args)
|
||||
}
|
||||
|
||||
5
renderer/lib/hx.js
Normal file
5
renderer/lib/hx.js
Normal file
@@ -0,0 +1,5 @@
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
module.exports = hx
|
||||
@@ -4,81 +4,123 @@ function LocationHistory () {
|
||||
if (!new.target) return new LocationHistory()
|
||||
this._history = []
|
||||
this._forward = []
|
||||
this._pending = null
|
||||
this._pending = false
|
||||
}
|
||||
|
||||
LocationHistory.prototype.go = function (page, cb) {
|
||||
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.url = function () {
|
||||
return this.current() && this.current().url
|
||||
}
|
||||
|
||||
LocationHistory.prototype.current = function () {
|
||||
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 () {
|
||||
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 () {
|
||||
return this._forward.length > 0
|
||||
}
|
||||
|
||||
LocationHistory.prototype.pending = function () {
|
||||
return this._pending
|
||||
LocationHistory.prototype.clearForward = function (url) {
|
||||
if (url == null) {
|
||||
this._forward = []
|
||||
} else {
|
||||
console.log(this._forward)
|
||||
console.log(url)
|
||||
this._forward = this._forward.filter(function (page) {
|
||||
return page.url !== url
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
LocationHistory.prototype.clearPending = function () {
|
||||
this._pending = null
|
||||
LocationHistory.prototype.backToFirst = function (cb) {
|
||||
var self = this
|
||||
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 () {}
|
||||
|
||||
90
renderer/lib/migrations.js
Normal file
90
renderer/lib/migrations.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
module.exports = {
|
||||
run
|
||||
}
|
||||
|
||||
var semver = require('semver')
|
||||
var config = require('../../config')
|
||||
|
||||
// Change `state.saved` (which will be saved back to config.json on exit) as
|
||||
// needed, for example to deal with config.json format changes across versions
|
||||
function run (state) {
|
||||
// Replace "{ version: 1 }" with app version (semver)
|
||||
if (!semver.valid(state.saved.version)) {
|
||||
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
|
||||
}
|
||||
|
||||
var version = state.saved.version
|
||||
|
||||
if (semver.lt(version, '0.7.0')) {
|
||||
migrate_0_7_0(state)
|
||||
}
|
||||
|
||||
// Future migrations...
|
||||
// if (semver.lt(version, '0.8.0')) {
|
||||
// migrate_0_8_0(state)
|
||||
// }
|
||||
|
||||
// Config is now on the new version
|
||||
state.saved.version = config.APP_VERSION
|
||||
}
|
||||
|
||||
function migrate_0_7_0 (state) {
|
||||
console.log('migrate to 0.7.0')
|
||||
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
|
||||
state.saved.torrents.forEach(function (ts) {
|
||||
var infoHash = ts.infoHash
|
||||
|
||||
// Replace torrentPath with torrentFileName
|
||||
var src, dst
|
||||
if (ts.torrentPath) {
|
||||
// There are a number of cases to handle here:
|
||||
// * Originally we used absolute paths
|
||||
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
||||
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
||||
// * Finally, now we're getting rid of torrentPath altogether
|
||||
console.log('replacing torrentPath %s', ts.torrentPath)
|
||||
if (path.isAbsolute(ts.torrentPath)) {
|
||||
src = ts.torrentPath
|
||||
} else if (ts.torrentPath.startsWith('..')) {
|
||||
src = ts.torrentPath
|
||||
} else {
|
||||
src = path.join(config.STATIC_PATH, ts.torrentPath)
|
||||
}
|
||||
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.torrentPath
|
||||
ts.torrentFileName = infoHash + '.torrent'
|
||||
}
|
||||
|
||||
// Replace posterURL with posterFileName
|
||||
if (ts.posterURL) {
|
||||
console.log('replacing posterURL %s', ts.posterURL)
|
||||
var extension = path.extname(ts.posterURL)
|
||||
src = path.isAbsolute(ts.posterURL)
|
||||
? ts.posterURL
|
||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||
dst = path.join(config.CONFIG_POSTER_PATH, infoHash + extension)
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.posterURL
|
||||
ts.posterFileName = infoHash + extension
|
||||
}
|
||||
|
||||
// Fix exception caused by incorrect file ordering.
|
||||
// https://github.com/feross/webtorrent-desktop/pull/604#issuecomment-222805214
|
||||
delete ts.defaultPlayFileIndex
|
||||
delete ts.files
|
||||
delete ts.selections
|
||||
delete ts.fileModtimes
|
||||
})
|
||||
}
|
||||
@@ -3,14 +3,13 @@ var path = require('path')
|
||||
|
||||
var remote = electron.remote
|
||||
|
||||
var config = require('../config')
|
||||
var LocationHistory = require('./lib/location-history')
|
||||
var config = require('../../config')
|
||||
var LocationHistory = require('./location-history')
|
||||
|
||||
module.exports = {
|
||||
getInitialState,
|
||||
getDefaultPlayState,
|
||||
getDefaultSavedState,
|
||||
getPlayingTorrentSummary
|
||||
getDefaultSavedState
|
||||
}
|
||||
|
||||
function getInitialState () {
|
||||
@@ -63,7 +62,8 @@ function getInitialState () {
|
||||
/*
|
||||
* Getters, for convenience
|
||||
*/
|
||||
getPlayingTorrentSummary
|
||||
getPlayingTorrentSummary,
|
||||
getPlayingFileSummary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ function getDefaultPlayState () {
|
||||
isStalled: false,
|
||||
lastTimeUpdate: 0, /* Unix time in ms */
|
||||
mouseStationarySince: 0, /* Unix time in ms */
|
||||
playbackRate: 1,
|
||||
subtitles: {
|
||||
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
||||
selectedIndex: -1, /* current subtitle track */
|
||||
@@ -92,7 +93,7 @@ function getDefaultPlayState () {
|
||||
/* If the saved state file doesn't exist yet, here's what we use instead */
|
||||
function getDefaultSavedState () {
|
||||
return {
|
||||
version: 1, /* make sure we can upgrade gracefully later */
|
||||
version: config.APP_VERSION, /* make sure we can upgrade gracefully later */
|
||||
torrents: [
|
||||
{
|
||||
status: 'paused',
|
||||
@@ -265,9 +266,11 @@ function getDefaultSavedState () {
|
||||
]
|
||||
}
|
||||
],
|
||||
downloadPath: config.IS_PORTABLE
|
||||
? path.join(config.CONFIG_PATH, 'Downloads')
|
||||
: remote.app.getPath('downloads')
|
||||
prefs: {
|
||||
downloadPath: config.IS_PORTABLE
|
||||
? path.join(config.CONFIG_PATH, 'Downloads')
|
||||
: remote.app.getPath('downloads')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,3 +278,9 @@ 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]
|
||||
}
|
||||
@@ -4,12 +4,18 @@ var captureVideoFrame = require('./capture-video-frame')
|
||||
var path = require('path')
|
||||
|
||||
function torrentPoster (torrent, cb) {
|
||||
// First, try to use the largest video file
|
||||
// First, try to use a poster image if available
|
||||
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
|
||||
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||
|
||||
// Second, try to use the largest image file
|
||||
// Third, try to use the largest image file
|
||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
|
||||
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
||||
|
||||
|
||||
@@ -50,19 +50,22 @@ table {
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.app {
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
animation: fadein 0.3s;
|
||||
background: rgb(40, 40, 40);
|
||||
animation: fadein 0.5s;
|
||||
}
|
||||
|
||||
.app:not(.is-focused) {
|
||||
@@ -94,11 +97,20 @@ table {
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
opacity: 0.85;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.icon.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.icon:not(.disabled):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* UTILITY CLASSES
|
||||
*/
|
||||
@@ -109,8 +121,8 @@ table {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.3;
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
@@ -144,8 +156,8 @@ table {
|
||||
.header {
|
||||
background: rgb(40, 40, 40);
|
||||
border-bottom: 1px solid rgb(20, 20, 20);
|
||||
height: 37px; /* vertically center OS menu buttons (OS X) */
|
||||
padding-top: 6px;
|
||||
height: 38px; /* vertically center OS menu buttons (OS X) */
|
||||
padding-top: 7px;
|
||||
overflow: hidden;
|
||||
flex: 0 1 auto;
|
||||
opacity: 1;
|
||||
@@ -164,7 +176,13 @@ table {
|
||||
}
|
||||
|
||||
.app.view-player .header {
|
||||
opacity: 0.8;
|
||||
background: rgba(40, 40, 40, 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 {
|
||||
@@ -172,12 +190,8 @@ table {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.app.hide-header .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
opacity: 0.6;
|
||||
opacity: 0.7;
|
||||
position: absolute;
|
||||
margin-top: 1px;
|
||||
padding: 0 150px 0 150px;
|
||||
@@ -188,35 +202,22 @@ table {
|
||||
|
||||
.header .nav {
|
||||
font-weight: bold;
|
||||
margin-right: 9px;
|
||||
}
|
||||
|
||||
.header .nav.left {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.header .nav.right {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.app.is-darwin:not(.is-fullscreen) .header .nav.left {
|
||||
margin-left: 78px;
|
||||
}
|
||||
|
||||
.header .nav.right {
|
||||
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 {
|
||||
.header .back,
|
||||
.header .forward {
|
||||
font-size: 30px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
@@ -279,36 +280,36 @@ table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-torrent-page {
|
||||
.create-torrent {
|
||||
padding: 10px 25px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute {
|
||||
.create-torrent .torrent-attribute {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute>* {
|
||||
.create-torrent .torrent-attribute>* {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute label {
|
||||
.create-torrent .torrent-attribute label {
|
||||
width: 60px;
|
||||
margin-right: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute>div {
|
||||
.create-torrent .torrent-attribute>div {
|
||||
width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute div {
|
||||
.create-torrent .torrent-attribute div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.create-torrent-page .torrent-attribute textarea {
|
||||
.create-torrent .torrent-attribute textarea {
|
||||
width: calc(100% - 80px);
|
||||
height: 80px;
|
||||
color: #eee;
|
||||
@@ -320,11 +321,11 @@ table {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.create-torrent-page textarea.torrent-trackers {
|
||||
.create-torrent textarea.torrent-trackers {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.create-torrent-page input.torrent-is-private {
|
||||
.create-torrent input.torrent-is-private {
|
||||
width: initial;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -535,6 +536,11 @@ input {
|
||||
}
|
||||
}
|
||||
|
||||
.torrent .buttons .play.resume-position {
|
||||
position: relative;
|
||||
-webkit-clip-path: circle(18px at center);
|
||||
}
|
||||
|
||||
.torrent .buttons .delete {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -543,6 +549,10 @@ input {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.torrent .buttons .radial-progress {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.torrent .name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
@@ -592,7 +602,11 @@ body.drag .app::after {
|
||||
}
|
||||
|
||||
.torrent-details {
|
||||
padding: 8em 12px 20px 20px;
|
||||
padding: 8em 0 20px 0;
|
||||
}
|
||||
|
||||
.torrent-details .warning {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.torrent-details table {
|
||||
@@ -617,7 +631,7 @@ body.drag .app::after {
|
||||
.torrent-details td {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
vertical-align: bottom;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.torrent-details td .icon {
|
||||
@@ -627,7 +641,14 @@ body.drag .app::after {
|
||||
}
|
||||
|
||||
.torrent-details td.col-icon {
|
||||
width: 2em;
|
||||
width: 3em;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.torrent-details td.col-icon .radial-progress {
|
||||
position: absolute;
|
||||
margin-top: 4px;
|
||||
margin-left: 0.5px;
|
||||
}
|
||||
|
||||
.torrent-details td.col-name {
|
||||
@@ -646,7 +667,8 @@ body.drag .app::after {
|
||||
}
|
||||
|
||||
.torrent-details td.col-select {
|
||||
width: 2em;
|
||||
width: 3em;
|
||||
padding-right: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -678,7 +700,7 @@ body.drag .app::after {
|
||||
* PLAYER CONTROLS
|
||||
*/
|
||||
|
||||
.player-controls {
|
||||
.player .controls {
|
||||
position: fixed;
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
width: 100%;
|
||||
@@ -687,7 +709,60 @@ body.drag .app::after {
|
||||
transition: opacity 0.15s ease-out;
|
||||
}
|
||||
|
||||
.app.hide-video-controls .player-controls {
|
||||
.player .controls .icon {
|
||||
display: block;
|
||||
margin: 8px;
|
||||
font-size: 22px;
|
||||
opacity: 0.85;
|
||||
|
||||
/* Make all icons have uniform spacing */
|
||||
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-caption {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -695,13 +770,16 @@ body.drag .app::after {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.app.hide-video-controls .player .player-controls:hover {
|
||||
/* TODO: find better way to handle this (that also
|
||||
* keeps the header visible too).
|
||||
*/
|
||||
.app.hide-video-controls .player .controls:hover {
|
||||
opacity: 1;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* invisible click target for scrubbing */
|
||||
.player-controls .scrub-bar {
|
||||
.player .controls .scrub-bar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 23px; /* 3px .loading-bar plus 10px above and below */
|
||||
@@ -710,7 +788,7 @@ body.drag .app::after {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.player-controls .loading-bar {
|
||||
.player .controls .loading-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
top: -3px;
|
||||
@@ -720,14 +798,14 @@ body.drag .app::after {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.player-controls .loading-bar-part {
|
||||
.player .controls .loading-bar-part {
|
||||
position: absolute;
|
||||
background-color: #dd0000;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.player-controls .playback-cursor {
|
||||
.player .controls .playback-cursor {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
background-color: #FFF;
|
||||
@@ -736,94 +814,26 @@ body.drag .app::after {
|
||||
border-radius: 50%;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
transition-property: width, height, border-radius, margin-top, margin-left;
|
||||
transition-property: width, height, top, margin-left;
|
||||
transition-duration: 0.1s;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.player-controls .play-pause {
|
||||
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;
|
||||
height: 20px;
|
||||
margin: 5px;
|
||||
|
||||
/*
|
||||
* Fix for overflowing captions icon
|
||||
* https://github.com/feross/webtorrent-desktop/issues/467
|
||||
*/
|
||||
max-width: 22px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.player-controls .volume,
|
||||
.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 {
|
||||
.player .controls .closed-caption.active,
|
||||
.player .controls .device.active {
|
||||
color: #9af;
|
||||
}
|
||||
|
||||
.player-controls .volume {
|
||||
display: block;
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.player-controls .volume-icon {
|
||||
float: left;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.player-controls .volume-slider {
|
||||
.player .controls .volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 50px;
|
||||
height: 3px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
vertical-align: sub;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.player-controls .volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
background-color: #fff;
|
||||
opacity: 1.0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 1px solid #303233;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.player-controls .volume-slider:focus {
|
||||
.player .controls .volume-slider:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -833,19 +843,49 @@ body.drag .app::after {
|
||||
|
||||
.player .playback-bar:hover .playback-cursor {
|
||||
top: -8px;
|
||||
margin-left: -5px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.player .controls .subtitles-list {
|
||||
position: fixed;
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
min-width: 100px;
|
||||
bottom: 45px;
|
||||
right: 3px;
|
||||
transition: opacity 0.15s ease-out;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.player .controls .subtitles-list .icon {
|
||||
display: inline;
|
||||
font-size: 17px;
|
||||
vertical-align: bottom;
|
||||
line-height: 21px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
background: none;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* CHROMECAST / AIRPLAY CONTROLS
|
||||
*/
|
||||
@@ -873,28 +913,172 @@ body.drag .app::after {
|
||||
}
|
||||
|
||||
/*
|
||||
* Subtitles list
|
||||
* Preferences page, based on Atom settings style
|
||||
*/
|
||||
|
||||
.subtitles-list {
|
||||
position: fixed;
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
min-width: 100px;
|
||||
bottom: 45px;
|
||||
right: 3px;
|
||||
transition: opacity 0.15s ease-out;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
.preferences {
|
||||
font-size: 12px;
|
||||
line-height: calc(10/7);
|
||||
}
|
||||
|
||||
.subtitles-list i {
|
||||
font-size: 11px; /* make the cast icons less huge */
|
||||
margin-right: 4px !important;
|
||||
.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
|
||||
*/
|
||||
@@ -963,10 +1147,6 @@ body.drag .app::after {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.app.hide-header .error-popover {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.error-popover.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -994,3 +1174,66 @@ body.drag .app::after {
|
||||
.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;
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="index.css" charset="utf-8">
|
||||
<link rel="stylesheet" href="main.css" charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<script async src="index.js"></script>
|
||||
<script async src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,23 +3,12 @@ console.time('init')
|
||||
var crashReporter = require('../crash-reporter')
|
||||
crashReporter.init()
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
// Electron apps have two processes: a main process (node) runs first and starts
|
||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||
// and this IPC channel receives from and sends messages to the main process
|
||||
var ipcRenderer = electron.ipcRenderer
|
||||
|
||||
// Listen for messages from the main process
|
||||
setupIpc()
|
||||
|
||||
var appConfig = require('application-config')('WebTorrent')
|
||||
var Async = require('async')
|
||||
var concat = require('concat-stream')
|
||||
var dragDrop = require('drag-drop')
|
||||
var electron = require('electron')
|
||||
var fs = require('fs-extra')
|
||||
var iso639 = require('iso-639-1')
|
||||
var mainLoop = require('main-loop')
|
||||
var parallel = require('run-parallel')
|
||||
var path = require('path')
|
||||
|
||||
var createElement = require('virtual-dom/create-element')
|
||||
@@ -29,8 +18,9 @@ var patch = require('virtual-dom/patch')
|
||||
var App = require('./views/app')
|
||||
var config = require('../config')
|
||||
var errors = require('./lib/errors')
|
||||
var migrations = require('./lib/migrations')
|
||||
var sound = require('./lib/sound')
|
||||
var State = require('./state')
|
||||
var State = require('./lib/state')
|
||||
var TorrentPlayer = require('./lib/torrent-player')
|
||||
var TorrentSummary = require('./lib/torrent-summary')
|
||||
|
||||
@@ -42,18 +32,34 @@ appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||
// This dependency is the slowest-loading, so we lazy load it
|
||||
var Cast = null
|
||||
|
||||
// For easy debugging in Developer Tools
|
||||
var state = global.state = State.getInitialState()
|
||||
|
||||
// Push the first page into the location history
|
||||
state.location.go({ url: 'home' })
|
||||
|
||||
var vdomLoop
|
||||
|
||||
var state = State.getInitialState()
|
||||
state.location.go({ url: 'home' }) // Add first page to location history
|
||||
|
||||
// Electron apps have two processes: a main process (node) runs first and starts
|
||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||
// and this IPC channel receives from and sends messages to the main process
|
||||
var ipcRenderer = electron.ipcRenderer
|
||||
|
||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
||||
// All other state is ephemeral. First we load state.saved then initialize the app.
|
||||
loadState(init)
|
||||
|
||||
function loadState (cb) {
|
||||
appConfig.read(function (err, data) {
|
||||
if (err) console.error(err)
|
||||
|
||||
// populate defaults if they're not there
|
||||
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
|
||||
state.saved.torrents.forEach(function (torrentSummary) {
|
||||
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
|
||||
})
|
||||
|
||||
if (cb) cb()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once when the application loads. (Not once per window.)
|
||||
* Connects to the torrent networks, sets up the UI and OS integrations like
|
||||
@@ -61,7 +67,7 @@ loadState(init)
|
||||
*/
|
||||
function init () {
|
||||
// Clean up the freshly-loaded config file, which may be from an older version
|
||||
cleanUpConfig()
|
||||
migrations.run(state)
|
||||
|
||||
// Restart everything we were torrenting last time the app ran
|
||||
resumeTorrents()
|
||||
@@ -80,6 +86,9 @@ function init () {
|
||||
})
|
||||
document.body.appendChild(vdomLoop.target)
|
||||
|
||||
// Listen for messages from the main process
|
||||
setupIpc()
|
||||
|
||||
// Calling update() updates the UI given the current state
|
||||
// Do this at least once a second to give every file in every torrentSummary
|
||||
// a progress bar and to keep the cursor in sync when playing a video
|
||||
@@ -87,7 +96,7 @@ function init () {
|
||||
|
||||
// OS integrations:
|
||||
// ...drag and drop a torrent or video file to play or seed
|
||||
dragDrop('body', (files) => dispatch('onOpen', files))
|
||||
dragDrop('body', onOpen)
|
||||
|
||||
// ...same thing if you paste a torrent
|
||||
document.addEventListener('paste', onPaste)
|
||||
@@ -96,9 +105,9 @@ function init () {
|
||||
window.addEventListener('focus', onFocus)
|
||||
window.addEventListener('blur', onBlur)
|
||||
|
||||
// Done! Ideally we want to get here <100ms after the user clicks the app
|
||||
sound.play('STARTUP')
|
||||
|
||||
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||
console.timeEnd('init')
|
||||
}
|
||||
|
||||
@@ -107,60 +116,6 @@ function delayedInit () {
|
||||
sound.preload()
|
||||
}
|
||||
|
||||
// Change `state.saved` (which will be saved back to config.json on exit) as
|
||||
// needed, for example to deal with config.json format changes across versions
|
||||
function cleanUpConfig () {
|
||||
state.saved.torrents.forEach(function (ts) {
|
||||
var infoHash = ts.infoHash
|
||||
|
||||
// Migration: replace torrentPath with torrentFileName
|
||||
var src, dst
|
||||
if (ts.torrentPath) {
|
||||
// There are a number of cases to handle here:
|
||||
// * Originally we used absolute paths
|
||||
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
||||
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
||||
// * Finally, now we're getting rid of torrentPath altogether
|
||||
console.log('migration: replacing torrentPath %s', ts.torrentPath)
|
||||
if (path.isAbsolute(ts.torrentPath)) {
|
||||
src = ts.torrentPath
|
||||
} else if (ts.torrentPath.startsWith('..')) {
|
||||
src = ts.torrentPath
|
||||
} else {
|
||||
src = path.join(config.STATIC_PATH, ts.torrentPath)
|
||||
}
|
||||
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.torrentPath
|
||||
ts.torrentFileName = infoHash + '.torrent'
|
||||
}
|
||||
|
||||
// Migration: replace posterURL with posterFileName
|
||||
if (ts.posterURL) {
|
||||
console.log('migration: replacing posterURL %s', ts.posterURL)
|
||||
var extension = path.extname(ts.posterURL)
|
||||
src = path.isAbsolute(ts.posterURL)
|
||||
? ts.posterURL
|
||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||
dst = path.join(config.CONFIG_POSTER_PATH, infoHash + extension)
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.posterURL
|
||||
ts.posterFileName = infoHash + extension
|
||||
}
|
||||
|
||||
// Migration: add per-file selections
|
||||
if (!ts.selections) {
|
||||
ts.selections = ts.files.map((x) => true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Lazily loads Chromecast and Airplay support
|
||||
function lazyLoadCast () {
|
||||
if (!Cast) {
|
||||
@@ -215,17 +170,24 @@ function dispatch (action, ...args) {
|
||||
if (action === 'addTorrent') {
|
||||
addTorrent(args[0] /* torrent */)
|
||||
}
|
||||
if (action === 'showOpenTorrentFile') {
|
||||
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
|
||||
if (action === 'openTorrentFile') {
|
||||
ipcRenderer.send('openTorrentFile') /* open torrent file */
|
||||
}
|
||||
if (action === 'openFiles') {
|
||||
ipcRenderer.send('openFiles') /* add files with dialog */
|
||||
}
|
||||
if (action === 'showCreateTorrent') {
|
||||
showCreateTorrent(args[0] /* fileOrFolder */)
|
||||
showCreateTorrent(args[0] /* paths */)
|
||||
}
|
||||
if (action === 'openTorrentAddress') {
|
||||
state.modal = { id: 'open-torrent-address-modal' }
|
||||
update()
|
||||
}
|
||||
if (action === 'createTorrent') {
|
||||
createTorrent(args[0] /* options */)
|
||||
}
|
||||
if (action === 'openFile') {
|
||||
openFile(args[0] /* infoHash */, args[1] /* index */)
|
||||
if (action === 'openItem') {
|
||||
openItem(args[0] /* infoHash */, args[1] /* index */)
|
||||
}
|
||||
if (action === 'toggleTorrent') {
|
||||
toggleTorrent(args[0] /* infoHash */)
|
||||
@@ -252,16 +214,7 @@ function dispatch (action, ...args) {
|
||||
setDimensions(args[0] /* dimensions */)
|
||||
}
|
||||
if (action === 'backToList') {
|
||||
// Exit any modals and screens with a back button
|
||||
state.modal = null
|
||||
while (state.location.hasBack()) state.location.back()
|
||||
|
||||
// Work around virtual-dom issue: it doesn't expose its redraw function,
|
||||
// and only redraws on requestAnimationFrame(). That means when the user
|
||||
// closes the window (hide window / minimize to tray) and we want to pause
|
||||
// the video, we update the vdom but it keeps playing until you reopen!
|
||||
var mediaTag = document.querySelector('video,audio')
|
||||
if (mediaTag) mediaTag.pause()
|
||||
backToList()
|
||||
}
|
||||
if (action === 'escapeBack') {
|
||||
if (state.modal) {
|
||||
@@ -282,19 +235,17 @@ function dispatch (action, ...args) {
|
||||
playPause()
|
||||
}
|
||||
if (action === 'play') {
|
||||
if (state.location.pending()) return
|
||||
state.location.go({
|
||||
url: 'player',
|
||||
onbeforeload: function (cb) {
|
||||
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
|
||||
},
|
||||
onbeforeunload: closePlayer
|
||||
})
|
||||
play()
|
||||
playFile(args[0] /* infoHash */, args[1] /* index */)
|
||||
}
|
||||
if (action === 'playbackJump') {
|
||||
jumpToTime(args[0] /* seconds */)
|
||||
}
|
||||
if (action === 'skip') {
|
||||
jumpToTime(state.playing.currentTime + (args[0] /* direction */ * 10))
|
||||
}
|
||||
if (action === 'changePlaybackRate') {
|
||||
changePlaybackRate(args[0] /* direction */)
|
||||
}
|
||||
if (action === 'changeVolume') {
|
||||
changeVolume(args[0] /* increase */)
|
||||
}
|
||||
@@ -314,7 +265,7 @@ function dispatch (action, ...args) {
|
||||
state.playing.isStalled = true
|
||||
}
|
||||
if (action === 'mediaError') {
|
||||
if (state.location.current().url === 'player') {
|
||||
if (state.location.url() === 'player') {
|
||||
state.playing.location = 'error'
|
||||
ipcRenderer.send('checkForVLC')
|
||||
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
||||
@@ -348,6 +299,26 @@ function dispatch (action, ...args) {
|
||||
if (action === 'exitModal') {
|
||||
state.modal = null
|
||||
}
|
||||
if (action === 'preferences') {
|
||||
state.location.go({
|
||||
url: 'preferences',
|
||||
onbeforeload: function (cb) {
|
||||
// initialize preferences
|
||||
state.window.title = 'Preferences'
|
||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||
cb()
|
||||
},
|
||||
onbeforeunload: function (cb) {
|
||||
// save state after preferences
|
||||
savePreferences()
|
||||
state.window.title = config.APP_WINDOW_TITLE
|
||||
cb()
|
||||
}
|
||||
})
|
||||
}
|
||||
if (action === 'updatePreferences') {
|
||||
updatePreferences(args[0], args[1] /* property, value */)
|
||||
}
|
||||
if (action === 'updateAvailable') {
|
||||
updateAvailable(args[0] /* version */)
|
||||
}
|
||||
@@ -359,6 +330,9 @@ function dispatch (action, ...args) {
|
||||
if (action === 'saveState') {
|
||||
saveState()
|
||||
}
|
||||
if (action === 'setTitle') {
|
||||
state.window.title = args[0] /* title */
|
||||
}
|
||||
|
||||
// Update the virtual-dom, unless it's just a mouse move event
|
||||
if (action !== 'mediaMouseMoved' || showOrHidePlayerControls()) {
|
||||
@@ -394,7 +368,7 @@ function pause () {
|
||||
}
|
||||
|
||||
function playPause () {
|
||||
if (state.location.current().url !== 'player') return
|
||||
if (state.location.url() !== 'player') return
|
||||
if (state.playing.isPaused) {
|
||||
play()
|
||||
} else {
|
||||
@@ -409,7 +383,26 @@ function jumpToTime (time) {
|
||||
state.playing.jumpToTime = time
|
||||
}
|
||||
}
|
||||
|
||||
function changePlaybackRate (direction) {
|
||||
var rate = state.playing.playbackRate
|
||||
if (direction > 0 && rate >= 0.25 && rate < 2) {
|
||||
rate += 0.25
|
||||
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
||||
rate -= 0.25
|
||||
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
|
||||
rate = -1
|
||||
} else if (direction > 0 && rate === -1) {
|
||||
rate = 0.25
|
||||
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
|
||||
rate *= 2
|
||||
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
|
||||
rate /= 2
|
||||
}
|
||||
state.playing.playbackRate = rate
|
||||
if (lazyLoadCast().isCasting() && !Cast.setRate(rate)) {
|
||||
state.playing.playbackRate = 1
|
||||
}
|
||||
}
|
||||
function changeVolume (delta) {
|
||||
// change volume with delta value
|
||||
setVolume(state.playing.volume + delta)
|
||||
@@ -436,6 +429,24 @@ function openSubtitles () {
|
||||
})
|
||||
}
|
||||
|
||||
// Quits any modal popovers and returns to the torrent list screen
|
||||
function backToList () {
|
||||
// Exit any modals and screens with a back button
|
||||
state.modal = null
|
||||
state.location.backToFirst(function () {
|
||||
// If we were already on the torrent list, scroll to the top
|
||||
var contentTag = document.querySelector('.content')
|
||||
if (contentTag) contentTag.scrollTop = 0
|
||||
|
||||
// Work around virtual-dom issue: it doesn't expose its redraw function,
|
||||
// and only redraws on requestAnimationFrame(). That means when the user
|
||||
// closes the window (hide window / minimize to tray) and we want to pause
|
||||
// the video, we update the vdom but it keeps playing until you reopen!
|
||||
var mediaTag = document.querySelector('video,audio')
|
||||
if (mediaTag) mediaTag.pause()
|
||||
})
|
||||
}
|
||||
|
||||
// Checks whether we are connected and already casting
|
||||
// Returns false if we not casting (state.playing.location === 'local')
|
||||
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
|
||||
@@ -446,18 +457,11 @@ function isCasting () {
|
||||
}
|
||||
|
||||
function setupIpc () {
|
||||
ipcRenderer.send('ipcReady')
|
||||
|
||||
ipcRenderer.on('log', (e, ...args) => console.log(...args))
|
||||
ipcRenderer.on('error', (e, ...args) => console.error(...args))
|
||||
|
||||
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
|
||||
|
||||
ipcRenderer.on('showOpenTorrentAddress', function (e) {
|
||||
state.modal = { id: 'open-torrent-address-modal' }
|
||||
update()
|
||||
})
|
||||
|
||||
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
|
||||
state.window.isFullScreen = isFullScreen
|
||||
if (!isFullScreen) {
|
||||
@@ -479,29 +483,36 @@ function setupIpc () {
|
||||
ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args))
|
||||
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
|
||||
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
||||
}
|
||||
|
||||
// Load state.saved from the JSON state file
|
||||
function loadState (cb) {
|
||||
appConfig.read(function (err, data) {
|
||||
if (err) console.error(err)
|
||||
console.log('loaded state from ' + appConfig.filePath)
|
||||
|
||||
// populate defaults if they're not there
|
||||
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
|
||||
state.saved.torrents.forEach(function (torrentSummary) {
|
||||
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
|
||||
})
|
||||
|
||||
if (cb) cb()
|
||||
})
|
||||
ipcRenderer.send('ipcReady')
|
||||
}
|
||||
|
||||
// Starts all torrents that aren't paused on program startup
|
||||
function resumeTorrents () {
|
||||
state.saved.torrents
|
||||
.filter((x) => x.status !== 'paused')
|
||||
.forEach((x) => startTorrentingSummary(x))
|
||||
.filter((torrentSummary) => torrentSummary.status !== 'paused')
|
||||
.forEach((torrentSummary) => startTorrentingSummary(torrentSummary))
|
||||
}
|
||||
|
||||
// Updates a single property in the UNSAVED prefs
|
||||
// For example: updatePreferences("foo.bar", "baz")
|
||||
// Call savePreferences to save to config.json
|
||||
function updatePreferences (property, value) {
|
||||
var path = property.split('.')
|
||||
var key = state.unsaved.prefs
|
||||
for (var i = 0; i < path.length - 1; i++) {
|
||||
if (typeof key[path[i]] === 'undefined') {
|
||||
key[path[i]] = {}
|
||||
}
|
||||
key = key[path[i]]
|
||||
}
|
||||
key[path[i]] = value
|
||||
}
|
||||
|
||||
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||
function savePreferences () {
|
||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||
saveState()
|
||||
}
|
||||
|
||||
// Don't write state.saved to file more than once a second
|
||||
@@ -529,7 +540,7 @@ function saveState () {
|
||||
if (key === 'progress' || key === 'torrentKey') {
|
||||
continue // Don't save progress info or key for the webtorrent process
|
||||
}
|
||||
if (key === 'playStatus' && x.playStatus !== 'unplayable') {
|
||||
if (key === 'playStatus') {
|
||||
continue // Don't save whether a torrent is playing / pending
|
||||
}
|
||||
torrent[key] = x[key]
|
||||
@@ -546,26 +557,33 @@ function saveState () {
|
||||
update()
|
||||
}
|
||||
|
||||
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
|
||||
// via any method (drag-drop, drag to app icon, command line)
|
||||
function onOpen (files) {
|
||||
if (!Array.isArray(files)) files = [ files ]
|
||||
|
||||
// In the player, the only drag-drop function is adding subtitles
|
||||
var isInPlayer = state.location.current().url === 'player'
|
||||
if (isInPlayer) {
|
||||
return addSubtitles(files.filter(isSubtitle), true)
|
||||
if (state.modal) {
|
||||
state.modal = null
|
||||
}
|
||||
|
||||
// Otherwise, you can only drag-drop onto the home screen
|
||||
var isHome = state.location.current().url === 'home' && !state.modal
|
||||
if (isHome) {
|
||||
var subtitles = files.filter(isSubtitle)
|
||||
|
||||
if (state.location.url() === 'home' || subtitles.length === 0) {
|
||||
if (files.every(isTorrent)) {
|
||||
// All .torrent files? Start downloading
|
||||
if (state.location.url() !== 'home') {
|
||||
backToList()
|
||||
}
|
||||
// All .torrent files? Add them.
|
||||
files.forEach(addTorrent)
|
||||
} else {
|
||||
// Show the Create Torrent screen. Let's seed those files.
|
||||
showCreateTorrent(files)
|
||||
}
|
||||
} else if (state.location.url() === 'player') {
|
||||
addSubtitles(subtitles, true)
|
||||
}
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
function isTorrent (file) {
|
||||
@@ -591,37 +609,46 @@ function getTorrentSummary (torrentKey) {
|
||||
|
||||
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
||||
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||
var instantIoRegex = /^(https:\/\/)?instant\.io\/#/
|
||||
function addTorrent (torrentId) {
|
||||
backToList()
|
||||
var torrentKey = state.nextTorrentKey++
|
||||
var path = state.saved.downloadPath
|
||||
var path = state.saved.prefs.downloadPath
|
||||
if (torrentId.path) {
|
||||
// Use path string instead of W3C File object
|
||||
torrentId = torrentId.path
|
||||
}
|
||||
// Allow a instant.io link to be pasted
|
||||
// TODO: remove this once support is added to webtorrent core
|
||||
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||
}
|
||||
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||
}
|
||||
|
||||
function addSubtitles (files, autoSelect) {
|
||||
// Subtitles are only supported while playing video
|
||||
// Subtitles are only supported when playing video files
|
||||
if (state.playing.type !== 'video') return
|
||||
if (files.length === 0) return
|
||||
|
||||
// Read the files concurrently, then add all resulting subtitle tracks
|
||||
console.log(files)
|
||||
var subs = state.playing.subtitles
|
||||
Async.map(files, loadSubtitle, function (err, tracks) {
|
||||
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||
parallel(tasks, function (err, tracks) {
|
||||
if (err) return onError(err)
|
||||
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
// No dupes allowed
|
||||
var track = tracks[i]
|
||||
if (subs.tracks.some((t) => track.filePath === t.filePath)) continue
|
||||
if (state.playing.subtitles.tracks.some(
|
||||
(t) => track.filePath === t.filePath)) continue
|
||||
|
||||
// Add the track
|
||||
subs.tracks.push(track)
|
||||
state.playing.subtitles.tracks.push(track)
|
||||
|
||||
// If we're auto-selecting a track, try to find one in the user's language
|
||||
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||
state.playing.subtitles.selectedIndex = subs.tracks.length - 1
|
||||
state.playing.subtitles.selectedIndex =
|
||||
state.playing.subtitles.tracks.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,31 +658,33 @@ function addSubtitles (files, autoSelect) {
|
||||
}
|
||||
|
||||
function loadSubtitle (file, cb) {
|
||||
var srtToVtt = require('srt-to-vtt')
|
||||
var concat = require('simple-concat')
|
||||
var LanguageDetect = require('languagedetect')
|
||||
var srtToVtt = require('srt-to-vtt')
|
||||
|
||||
// Read the .SRT or .VTT file, parse it, add subtitle track
|
||||
var filePath = file.path || file
|
||||
fs.createReadStream(filePath).pipe(srtToVtt()).pipe(concat(function (buf) {
|
||||
|
||||
var vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
||||
|
||||
concat(vttStream, function (err, buf) {
|
||||
if (err) return onError(new Error('Error parsing subtitles file.'))
|
||||
|
||||
// Detect what language the subtitles are in
|
||||
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
||||
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
||||
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
||||
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
||||
|
||||
// Set the cue text position so it appears above the player controls.
|
||||
// The only way to change cue text position is by modifying the VTT. It is not
|
||||
// possible via CSS.
|
||||
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
|
||||
var track = {
|
||||
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
|
||||
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
||||
language: langDetected,
|
||||
label: langDetected,
|
||||
filePath: filePath
|
||||
}
|
||||
|
||||
cb(null, track)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function selectSubtitle (ix) {
|
||||
@@ -665,6 +694,7 @@ function selectSubtitle (ix) {
|
||||
// Checks whether a language name like "English" or "German" matches the system
|
||||
// language, aka the current locale
|
||||
function isSystemLanguage (language) {
|
||||
var iso639 = require('iso-639-1')
|
||||
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
|
||||
var langIso = iso639.getCode(language) // eg "de" if language is "German"
|
||||
return langIso === osLangISO
|
||||
@@ -707,7 +737,7 @@ function startTorrentingSummary (torrentSummary) {
|
||||
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
|
||||
|
||||
// Use Downloads folder by default
|
||||
var path = s.path || state.saved.downloadPath
|
||||
var path = s.path || state.saved.prefs.downloadPath
|
||||
|
||||
var torrentID
|
||||
if (s.torrentFileName) { // Load torrent file from disk
|
||||
@@ -727,8 +757,9 @@ function startTorrentingSummary (torrentSummary) {
|
||||
|
||||
// Shows the Create Torrent page with options to seed a given file or folder
|
||||
function showCreateTorrent (files) {
|
||||
if (Array.isArray(files)) {
|
||||
if (state.location.pending() || state.location.current().url !== 'home') return
|
||||
// Files will either be an array of file objects, which we can send directly
|
||||
// to the create-torrent screen
|
||||
if (files.length === 0 || typeof files[0] !== 'string') {
|
||||
state.location.go({
|
||||
url: 'create-torrent',
|
||||
files: files
|
||||
@@ -736,13 +767,29 @@ function showCreateTorrent (files) {
|
||||
return
|
||||
}
|
||||
|
||||
var fileOrFolder = files
|
||||
findFilesRecursive(fileOrFolder, showCreateTorrent)
|
||||
// ... or it will be an array of mixed file and folder paths. We have to walk
|
||||
// through all the folders and find the files
|
||||
findFilesRecursive(files, showCreateTorrent)
|
||||
}
|
||||
|
||||
// Recursively finds {name, path, size} for all files in a folder
|
||||
// Calls `cb` on success, calls `onError` on failure
|
||||
function findFilesRecursive (fileOrFolder, cb) {
|
||||
function findFilesRecursive (paths, cb) {
|
||||
if (paths.length > 1) {
|
||||
var numComplete = 0
|
||||
var ret = []
|
||||
paths.forEach(function (path) {
|
||||
findFilesRecursive([path], function (fileObjs) {
|
||||
ret = ret.concat(fileObjs)
|
||||
if (++numComplete === paths.length) {
|
||||
cb(ret)
|
||||
}
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var fileOrFolder = paths[0]
|
||||
fs.stat(fileOrFolder, function (err, stat) {
|
||||
if (err) return onError(err)
|
||||
|
||||
@@ -760,16 +807,8 @@ function findFilesRecursive (fileOrFolder, cb) {
|
||||
var folderPath = fileOrFolder
|
||||
fs.readdir(folderPath, function (err, fileNames) {
|
||||
if (err) return onError(err)
|
||||
var numComplete = 0
|
||||
var ret = []
|
||||
fileNames.forEach(function (fileName) {
|
||||
findFilesRecursive(path.join(folderPath, fileName), function (fileObjs) {
|
||||
ret = ret.concat(fileObjs)
|
||||
if (++numComplete === fileNames.length) {
|
||||
cb(ret)
|
||||
}
|
||||
})
|
||||
})
|
||||
var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
||||
findFilesRecursive(paths, cb)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -778,6 +817,9 @@ function findFilesRecursive (fileOrFolder, cb) {
|
||||
function createTorrent (options) {
|
||||
var torrentKey = state.nextTorrentKey++
|
||||
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||
state.location.backToFirst(function () {
|
||||
state.location.clearForward('create-torrent')
|
||||
})
|
||||
}
|
||||
|
||||
function torrentInfoHash (torrentKey, infoHash) {
|
||||
@@ -786,11 +828,17 @@ function torrentInfoHash (torrentKey, infoHash) {
|
||||
torrentSummary ? 'existing' : 'new', torrentKey)
|
||||
|
||||
if (!torrentSummary) {
|
||||
// Check if an existing (non-active) torrent has the same info hash
|
||||
if (state.saved.torrents.find((t) => t.infoHash === infoHash)) {
|
||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||
return onError(new Error('Cannot add duplicate torrent'))
|
||||
}
|
||||
|
||||
torrentSummary = {
|
||||
torrentKey: torrentKey,
|
||||
status: 'new'
|
||||
}
|
||||
state.saved.torrents.push(torrentSummary)
|
||||
state.saved.torrents.unshift(torrentSummary)
|
||||
sound.play('ADD')
|
||||
}
|
||||
|
||||
@@ -824,11 +872,17 @@ function torrentMetadata (torrentKey, torrentInfo) {
|
||||
torrentSummary.status = 'downloading'
|
||||
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
||||
torrentSummary.path = torrentInfo.path
|
||||
torrentSummary.files = torrentInfo.files
|
||||
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
||||
// For now, check whether torrentSummary.files has already been set:
|
||||
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
||||
if (!hasDetailedFileInfo) {
|
||||
torrentSummary.files = torrentInfo.files
|
||||
}
|
||||
if (!torrentSummary.selections) {
|
||||
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||
}
|
||||
torrentSummary.defaultPlayFileIndex = pickFileToPlay(torrentInfo.files)
|
||||
update()
|
||||
|
||||
// Save the .torrent file, if it hasn't been saved already
|
||||
@@ -880,7 +934,10 @@ function torrentProgress (progressInfo) {
|
||||
torrentSummary.progress = p
|
||||
})
|
||||
|
||||
checkForSubtitles()
|
||||
// TODO: Find an efficient way to re-enable this line, which allows subtitle
|
||||
// files which are completed after a video starts to play to be added
|
||||
// dynamically to the list of subtitles.
|
||||
// checkForSubtitles()
|
||||
|
||||
update()
|
||||
}
|
||||
@@ -937,11 +994,25 @@ function pickFileToPlay (files) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Opens the video player
|
||||
function playFile (infoHash, index) {
|
||||
state.location.go({
|
||||
url: 'player',
|
||||
onbeforeload: function (cb) {
|
||||
play()
|
||||
openPlayer(infoHash, index, cb)
|
||||
},
|
||||
onbeforeunload: closePlayer
|
||||
}, function (err) {
|
||||
if (err) onError(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Opens the video player to a specific torrent
|
||||
function openPlayer (infoHash, index, cb) {
|
||||
var torrentSummary = getTorrentSummary(infoHash)
|
||||
|
||||
// automatically choose which file in the torrent to play, if necessary
|
||||
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
|
||||
if (index === undefined) index = pickFileToPlay(torrentSummary.files)
|
||||
if (index === undefined) return cb(new errors.UnplayableError())
|
||||
|
||||
@@ -953,7 +1024,7 @@ function openPlayer (infoHash, index, cb) {
|
||||
var timeout = setTimeout(function () {
|
||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||
sound.play('ERROR')
|
||||
cb(new Error('playback timed out'))
|
||||
cb(new Error('Playback timed out. Try again.'))
|
||||
update()
|
||||
}, 10000) /* give it a few seconds */
|
||||
|
||||
@@ -976,6 +1047,15 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
||||
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||
: 'other'
|
||||
|
||||
// pick up where we left off
|
||||
if (fileSummary.currentTime) {
|
||||
var fraction = fileSummary.currentTime / fileSummary.duration
|
||||
var secondsLeft = fileSummary.duration - fileSummary.currentTime
|
||||
if (fraction < 0.9 && secondsLeft > 10) {
|
||||
state.playing.jumpToTime = fileSummary.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
// if it's audio, parse out the metadata (artist, title, etc)
|
||||
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
||||
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
||||
@@ -1014,6 +1094,9 @@ function closePlayer (cb) {
|
||||
ipcRenderer.send('vlcQuit')
|
||||
}
|
||||
state.window.title = config.APP_WINDOW_TITLE
|
||||
// Lets save volume for later
|
||||
state.previousVolume = state.playing.volume
|
||||
|
||||
state.playing = State.getDefaultPlayState()
|
||||
state.server = null
|
||||
|
||||
@@ -1030,7 +1113,7 @@ function closePlayer (cb) {
|
||||
cb()
|
||||
}
|
||||
|
||||
function openFile (infoHash, index) {
|
||||
function openItem (infoHash, index) {
|
||||
var torrentSummary = getTorrentSummary(infoHash)
|
||||
var filePath = path.join(
|
||||
torrentSummary.path,
|
||||
@@ -1059,7 +1142,7 @@ function deleteTorrent (infoHash) {
|
||||
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||
if (index > -1) state.saved.torrents.splice(index, 1)
|
||||
saveStateThrottled()
|
||||
state.location.clearForward() // prevent user from going forward to a deleted torrent
|
||||
state.location.clearForward('player') // prevent user from going forward to a deleted torrent
|
||||
sound.play('DELETE')
|
||||
}
|
||||
|
||||
@@ -1125,7 +1208,7 @@ function saveTorrentFileAs (torrentSummary) {
|
||||
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
||||
var opts = {
|
||||
title: 'Save Torrent File',
|
||||
defaultPath: path.join(state.saved.downloadPath, newFileName),
|
||||
defaultPath: path.join(state.saved.prefs.downloadPath, newFileName),
|
||||
filters: [
|
||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
@@ -1195,7 +1278,7 @@ function showDoneNotification (torrent) {
|
||||
})
|
||||
|
||||
notif.onclick = function () {
|
||||
ipcRenderer.send('focusWindow', 'main')
|
||||
ipcRenderer.send('show')
|
||||
}
|
||||
|
||||
sound.play('DONE')
|
||||
@@ -1207,7 +1290,7 @@ function showDoneNotification (torrent) {
|
||||
// * The video is paused
|
||||
// * The video is playing remotely on Chromecast or Airplay
|
||||
function showOrHidePlayerControls () {
|
||||
var hideControls = state.location.current().url === 'player' &&
|
||||
var hideControls = state.location.url() === 'player' &&
|
||||
state.playing.mouseStationarySince !== 0 &&
|
||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||
!state.playing.isPaused &&
|
||||
@@ -1242,8 +1325,10 @@ function onPaste (e) {
|
||||
torrentIds.forEach(function (torrentId) {
|
||||
torrentId = torrentId.trim()
|
||||
if (torrentId.length === 0) return
|
||||
dispatch('addTorrent', torrentId)
|
||||
addTorrent(torrentId)
|
||||
})
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
function onFocus (e) {
|
||||
@@ -1,15 +1,15 @@
|
||||
module.exports = App
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
var Header = require('./header')
|
||||
|
||||
var Views = {
|
||||
'home': require('./torrent-list'),
|
||||
'home': require('./home'),
|
||||
'player': require('./player'),
|
||||
'create-torrent': require('./create-torrent-page')
|
||||
'create-torrent': require('./create-torrent'),
|
||||
'preferences': require('./preferences')
|
||||
}
|
||||
|
||||
var Modals = {
|
||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||
'update-available-modal': require('./update-available-modal'),
|
||||
@@ -22,24 +22,20 @@ function App (state) {
|
||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||
// * The video is paused
|
||||
// * The video is playing remotely on Chromecast or Airplay
|
||||
var hideControls = state.location.current().url === 'player' &&
|
||||
var hideControls = state.location.url() === 'player' &&
|
||||
state.playing.mouseStationarySince !== 0 &&
|
||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||
!state.playing.isPaused &&
|
||||
state.playing.location === 'local'
|
||||
|
||||
// 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'
|
||||
state.playing.location === 'local' &&
|
||||
state.playing.playbackRate === 1
|
||||
|
||||
var cls = [
|
||||
'view-' + state.location.current().url, /* e.g. view-home, view-player */
|
||||
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
||||
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
||||
]
|
||||
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
||||
if (state.window.isFocused) cls.push('is-focused')
|
||||
if (hideControls) cls.push('hide-video-controls')
|
||||
if (hideHeader) cls.push('hide-header')
|
||||
|
||||
return hx`
|
||||
<div class='app ${cls.join(' ')}'>
|
||||
@@ -54,12 +50,13 @@ function App (state) {
|
||||
function getErrorPopover (state) {
|
||||
var now = new Date().getTime()
|
||||
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
||||
var hasErrors = recentErrors.length > 0
|
||||
|
||||
var errorElems = recentErrors.map(function (error) {
|
||||
return hx`<div class='error'>${error.message}</div>`
|
||||
})
|
||||
return hx`
|
||||
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
|
||||
<div class='error-popover ${hasErrors ? 'visible' : 'hidden'}'>
|
||||
<div class='title'>Error</div>
|
||||
${errorElems}
|
||||
</div>
|
||||
@@ -80,6 +77,6 @@ function getModal (state) {
|
||||
}
|
||||
|
||||
function getView (state) {
|
||||
var url = state.location.current().url
|
||||
var url = state.location.url()
|
||||
return Views[url](state)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
module.exports = CreateTorrentPage
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var createTorrent = require('create-torrent')
|
||||
var path = require('path')
|
||||
var prettyBytes = require('prettier-bytes')
|
||||
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function CreateTorrentPage (state) {
|
||||
var info = state.location.current()
|
||||
@@ -17,17 +14,14 @@ function CreateTorrentPage (state) {
|
||||
var files = info.files
|
||||
.filter((f) => !f.name.startsWith('.'))
|
||||
.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
|
||||
var pathPrefix = info.folderPath
|
||||
if (!pathPrefix) {
|
||||
if (files.length > 0) {
|
||||
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||
pathPrefix = path.dirname(pathPrefix)
|
||||
}
|
||||
} else {
|
||||
pathPrefix = files[0]
|
||||
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||
pathPrefix = path.dirname(pathPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +56,7 @@ function CreateTorrentPage (state) {
|
||||
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
|
||||
|
||||
return hx`
|
||||
<div class='create-torrent-page'>
|
||||
<div class='create-torrent'>
|
||||
<h2>Create torrent ${defaultName}</h2>
|
||||
<p class="torrent-info">
|
||||
${torrentInfo}
|
||||
@@ -119,11 +113,10 @@ function CreateTorrentPage (state) {
|
||||
comment: comment
|
||||
}
|
||||
dispatch('createTorrent', options)
|
||||
dispatch('backToList')
|
||||
}
|
||||
|
||||
function handleCancel () {
|
||||
dispatch('backToList')
|
||||
dispatch('back')
|
||||
}
|
||||
|
||||
function handleToggleShowAdvanced () {
|
||||
@@ -134,6 +127,27 @@ function CreateTorrentPage (state) {
|
||||
}
|
||||
}
|
||||
|
||||
function CreateTorrentErrorPage () {
|
||||
return hx`
|
||||
<div class='create-torrent'>
|
||||
<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
|
||||
function findCommonPrefix (a, b) {
|
||||
for (var i = 0; i < a.length && i < b.length; i++) {
|
||||
@@ -1,16 +1,13 @@
|
||||
module.exports = Header
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var {dispatcher} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function Header (state) {
|
||||
return hx`
|
||||
<div class='header'>
|
||||
${getTitle()}
|
||||
<div class='nav left'>
|
||||
<div class='nav left float-left'>
|
||||
<i.icon.back
|
||||
class=${state.location.hasBack() ? '' : 'disabled'}
|
||||
title='Back'
|
||||
@@ -24,7 +21,7 @@ function Header (state) {
|
||||
chevron_right
|
||||
</i>
|
||||
</div>
|
||||
<div class='nav right'>
|
||||
<div class='nav right float-right'>
|
||||
${getAddButton()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,12 +34,12 @@ function Header (state) {
|
||||
}
|
||||
|
||||
function getAddButton () {
|
||||
if (state.location.current().url !== 'player') {
|
||||
if (state.location.url() === 'home') {
|
||||
return hx`
|
||||
<i
|
||||
class='icon add'
|
||||
title='Add torrent'
|
||||
onclick=${dispatcher('showOpenTorrentFile')}>
|
||||
onclick=${dispatcher('openFiles')}>
|
||||
add
|
||||
</i>
|
||||
`
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
module.exports = TorrentList
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
var prettyBytes = require('prettier-bytes')
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
var TorrentSummary = require('../lib/torrent-summary')
|
||||
var TorrentPlayer = require('../lib/torrent-player')
|
||||
var {dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
function TorrentList (state) {
|
||||
var torrentRows = state.saved.torrents.map(
|
||||
(torrentSummary) => renderTorrent(torrentSummary))
|
||||
(torrentSummary) => renderTorrent(torrentSummary)
|
||||
)
|
||||
|
||||
return hx`
|
||||
<div class='torrent-list'>
|
||||
${torrentRows}
|
||||
@@ -20,11 +20,7 @@ function TorrentList (state) {
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
// Renders a torrent in the torrent list
|
||||
// Includes name, download status, play button, background image
|
||||
// May be expanded for additional info, including the list of files inside
|
||||
function renderTorrent (torrentSummary) {
|
||||
// Get ephemeral data (like progress %) directly from the WebTorrent handle
|
||||
var infoHash = torrentSummary.infoHash
|
||||
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||
|
||||
@@ -118,12 +114,7 @@ function TorrentList (state) {
|
||||
var infoHash = torrentSummary.infoHash
|
||||
|
||||
var playIcon, playTooltip, playClass
|
||||
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') {
|
||||
if (torrentSummary.playStatus === 'timeout') {
|
||||
playIcon = 'warning'
|
||||
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
||||
} else {
|
||||
@@ -143,6 +134,18 @@ function TorrentList (state) {
|
||||
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
|
||||
var playButton
|
||||
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
||||
@@ -158,6 +161,7 @@ function TorrentList (state) {
|
||||
|
||||
return hx`
|
||||
<div class='buttons'>
|
||||
${positionElem}
|
||||
${playButton}
|
||||
<i.button-round.icon.download
|
||||
class=${torrentSummary.status}
|
||||
@@ -186,11 +190,17 @@ function TorrentList (state) {
|
||||
filesElement = hx`<div class='files warning'>${message}</div>`
|
||||
} else {
|
||||
// We do know the files. List them and show download stats for each one
|
||||
var fileRows = torrentSummary.files.map(
|
||||
(file, index) => renderFileRow(torrentSummary, file, index))
|
||||
var fileRows = torrentSummary.files
|
||||
.map((file, index) => ({ file, index }))
|
||||
.sort(function (a, b) {
|
||||
if (a.file.name < b.file.name) return -1
|
||||
if (b.file.name < a.file.name) return 1
|
||||
return 0
|
||||
})
|
||||
.map((object) => renderFileRow(torrentSummary, object.file, object.index))
|
||||
|
||||
filesElement = hx`
|
||||
<div class='files'>
|
||||
<strong>Files</strong>
|
||||
<table>
|
||||
${fileRows}
|
||||
</table>
|
||||
@@ -217,7 +227,14 @@ function TorrentList (state) {
|
||||
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
||||
}
|
||||
|
||||
// Second, render the file as a table row
|
||||
// Second, for media files where we saved our position, show how far we got
|
||||
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 icon
|
||||
@@ -227,23 +244,24 @@ function TorrentList (state) {
|
||||
handleClick = dispatcher('play', infoHash, index)
|
||||
} else {
|
||||
icon = 'description' /* file icon, opens in OS default app */
|
||||
handleClick = dispatcher('openFile', infoHash, index)
|
||||
handleClick = dispatcher('openItem', infoHash, index)
|
||||
}
|
||||
var rowClass = ''
|
||||
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||
return hx`
|
||||
<tr>
|
||||
<td class='col-icon ${rowClass}' onclick=${handleClick}>
|
||||
<tr onclick=${handleClick}>
|
||||
<td class='col-icon ${rowClass}'>
|
||||
${positionElem}
|
||||
<i class='icon'>${icon}</i>
|
||||
</td>
|
||||
<td class='col-name ${rowClass}' onclick=${handleClick}>
|
||||
<td class='col-name ${rowClass}'>
|
||||
${file.name}
|
||||
</td>
|
||||
<td class='col-progress ${rowClass}' onclick=${handleClick}>
|
||||
<td class='col-progress ${rowClass}'>
|
||||
${isSelected ? progress : ''}
|
||||
</td>
|
||||
<td class='col-size ${rowClass}' onclick=${handleClick}>
|
||||
<td class='col-size ${rowClass}'>
|
||||
${prettyBytes(file.length)}
|
||||
</td>
|
||||
<td class='col-select'
|
||||
@@ -254,3 +272,24 @@ function TorrentList (state) {
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
`
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
module.exports = OpenTorrentAddressModal
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function OpenTorrentAddressModal (state) {
|
||||
return hx`
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
module.exports = Player
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var prettyBytes = require('prettier-bytes')
|
||||
var Bitfield = require('bitfield')
|
||||
var prettyBytes = require('prettier-bytes')
|
||||
var zeroFill = require('zero-fill')
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
var TorrentSummary = require('../lib/torrent-summary')
|
||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
@@ -36,7 +34,8 @@ function renderMedia (state) {
|
||||
|
||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||
// Instead, grab the DOM node and play/pause it if necessary
|
||||
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
|
||||
// Get the <video> or <audio> tag
|
||||
var mediaElement = document.querySelector(state.playing.type)
|
||||
if (mediaElement !== null) {
|
||||
if (state.playing.isPaused && !mediaElement.paused) {
|
||||
mediaElement.pause()
|
||||
@@ -48,6 +47,15 @@ function renderMedia (state) {
|
||||
mediaElement.currentTime = state.playing.jumpToTime
|
||||
state.playing.jumpToTime = null
|
||||
}
|
||||
if (state.playing.playbackRate !== mediaElement.playbackRate) {
|
||||
mediaElement.playbackRate = state.playing.playbackRate
|
||||
}
|
||||
// Recover previous volume
|
||||
if (state.previousVolume !== null && isFinite(state.previousVolume)) {
|
||||
mediaElement.volume = state.previousVolume
|
||||
state.previousVolume = null
|
||||
}
|
||||
|
||||
// Set volume
|
||||
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
||||
mediaElement.volume = state.playing.setVolume
|
||||
@@ -55,13 +63,16 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
// Switch to the newly added subtitle track, if available
|
||||
var tracks = mediaElement.textTracks
|
||||
var tracks = mediaElement.textTracks || []
|
||||
for (var j = 0; j < tracks.length; j++) {
|
||||
tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden'
|
||||
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
||||
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
|
||||
}
|
||||
|
||||
state.playing.currentTime = mediaElement.currentTime
|
||||
state.playing.duration = mediaElement.duration
|
||||
// Save video position
|
||||
var file = state.getPlayingFileSummary()
|
||||
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
||||
file.duration = state.playing.duration = mediaElement.duration
|
||||
state.playing.volume = mediaElement.volume
|
||||
}
|
||||
|
||||
@@ -108,7 +119,7 @@ function renderMedia (state) {
|
||||
</div>
|
||||
`
|
||||
|
||||
// As soon as the video loads enough to know the video dimensions, resize the window
|
||||
// As soon as we know the video dimensions, resize the window
|
||||
function onLoadedMetadata (e) {
|
||||
if (state.playing.type !== 'video') return
|
||||
var video = e.target
|
||||
@@ -125,12 +136,14 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
function onCanPlay (e) {
|
||||
var video = e.target
|
||||
if (video.webkitVideoDecodedByteCount > 0 &&
|
||||
video.webkitAudioDecodedByteCount === 0) {
|
||||
var elem = e.target
|
||||
if (state.playing.type === 'video' &&
|
||||
elem.webkitVideoDecodedByteCount === 0) {
|
||||
dispatch('mediaError', 'Video codec unsupported')
|
||||
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
||||
dispatch('mediaError', 'Audio codec unsupported')
|
||||
} else {
|
||||
video.play()
|
||||
elem.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +163,8 @@ function renderOverlay (state) {
|
||||
} else if (elems.length !== 0) {
|
||||
style = { backgroundImage: cssBackgroundImageDarkGradient() }
|
||||
} else {
|
||||
return /* Video, not audio, and it isn't stalled, so no spinner. No overlay needed. */
|
||||
// Video playing, so no spinner. No overlay needed
|
||||
return
|
||||
}
|
||||
|
||||
return hx`
|
||||
@@ -161,8 +175,7 @@ function renderOverlay (state) {
|
||||
}
|
||||
|
||||
function renderAudioMetadata (state) {
|
||||
var torrentSummary = state.getPlayingTorrentSummary()
|
||||
var fileSummary = torrentSummary.files[state.playing.fileIndex]
|
||||
var fileSummary = state.getPlayingFileSummary()
|
||||
if (!fileSummary.audioInfo) return
|
||||
var info = fileSummary.audioInfo
|
||||
|
||||
@@ -181,15 +194,37 @@ function renderAudioMetadata (state) {
|
||||
track = info.track.no + ' of ' + info.track.of
|
||||
}
|
||||
|
||||
// Show a small info box in the middle of the screen with title/album/artist/etc
|
||||
// Show a small info box in the middle of the screen with title/album/etc
|
||||
var elems = []
|
||||
if (artist) elems.push(hx`<div class='audio-artist'><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>`)
|
||||
if (artist) {
|
||||
elems.push(hx`
|
||||
<div class='audio-artist'>
|
||||
<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 artist/etc info, if available. Otherwise, center the title
|
||||
// Align the title with the other info, if available. Otherwise, center title
|
||||
var emptyLabel = hx`<label></label>`
|
||||
elems.unshift(hx`<div class='audio-title'>${elems.length ? emptyLabel : undefined}${title}</div>`)
|
||||
elems.unshift(hx`
|
||||
<div class='audio-title'>
|
||||
${elems.length ? emptyLabel : undefined}${title}
|
||||
</div>
|
||||
`)
|
||||
|
||||
return hx`<div class='audio-metadata'>${elems}</div>`
|
||||
}
|
||||
@@ -272,18 +307,19 @@ function renderSubtitlesOptions (state) {
|
||||
var isSelected = state.playing.subtitles.selectedIndex === ix
|
||||
return hx`
|
||||
<li onclick=${dispatcher('selectSubtitle', ix)}>
|
||||
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||
<i.icon>${'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
||||
${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>${noneSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||
<i.icon>${noneClass}</i>
|
||||
None
|
||||
</li>
|
||||
</ul>
|
||||
@@ -292,7 +328,7 @@ function renderSubtitlesOptions (state) {
|
||||
|
||||
function renderPlayerControls (state) {
|
||||
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
||||
? 'disabled'
|
||||
: state.playing.subtitles.selectedIndex >= 0
|
||||
@@ -303,15 +339,27 @@ function renderPlayerControls (state) {
|
||||
hx`
|
||||
<div class='playback-bar'>
|
||||
${renderLoadingBar(state)}
|
||||
<div class='playback-cursor' style=${playbackCursorStyle}></div>
|
||||
<div class='scrub-bar'
|
||||
<div
|
||||
class='playback-cursor'
|
||||
style=${playbackCursorStyle}>
|
||||
</div>
|
||||
<div
|
||||
class='scrub-bar'
|
||||
draggable='true'
|
||||
ondragstart=${handleDragStart}
|
||||
onclick=${handleScrub},
|
||||
ondrag=${handleScrub}></div>
|
||||
ondrag=${handleScrub}>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
hx`
|
||||
<i class='icon fullscreen'
|
||||
<i class='icon play-pause float-left' onclick=${dispatcher('playPause')}>
|
||||
${state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||
</i>
|
||||
`,
|
||||
hx`
|
||||
<i
|
||||
class='icon fullscreen float-right'
|
||||
onclick=${dispatcher('toggleFullScreen')}>
|
||||
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||
</i>
|
||||
@@ -321,10 +369,10 @@ function renderPlayerControls (state) {
|
||||
if (state.playing.type === 'video') {
|
||||
// show closed captions icon
|
||||
elements.push(hx`
|
||||
<i.icon.closed-captions
|
||||
<i.icon.closed-caption.float-right
|
||||
class=${captionsClass}
|
||||
onclick=${handleSubtitles}>
|
||||
closed_captions
|
||||
closed_caption
|
||||
</i>
|
||||
`)
|
||||
}
|
||||
@@ -333,7 +381,9 @@ function renderPlayerControls (state) {
|
||||
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
||||
var isOnAirplay = state.playing.location.startsWith('airplay')
|
||||
var isOnDlna = state.playing.location.startsWith('dlna')
|
||||
var chromecastClass, chromecastHandler, airplayClass, airplayHandler, dlnaClass, dlnaHandler
|
||||
var chromecastClass, chromecastHandler
|
||||
var airplayClass, airplayHandler
|
||||
var dlnaClass, dlnaHandler
|
||||
if (isOnChromecast) {
|
||||
chromecastClass = 'active'
|
||||
dlnaClass = 'disabled'
|
||||
@@ -366,7 +416,7 @@ function renderPlayerControls (state) {
|
||||
if (state.devices.chromecast || isOnChromecast) {
|
||||
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
|
||||
elements.push(hx`
|
||||
<i.icon.device
|
||||
<i.icon.device.float-right
|
||||
class=${chromecastClass}
|
||||
onclick=${chromecastHandler}>
|
||||
${castIcon}
|
||||
@@ -375,7 +425,7 @@ function renderPlayerControls (state) {
|
||||
}
|
||||
if (state.devices.airplay || isOnAirplay) {
|
||||
elements.push(hx`
|
||||
<i.icon.device
|
||||
<i.icon.device.float-right
|
||||
class=${airplayClass}
|
||||
onclick=${airplayHandler}>
|
||||
airplay
|
||||
@@ -384,7 +434,8 @@ function renderPlayerControls (state) {
|
||||
}
|
||||
if (state.devices.dlna || isOnDlna) {
|
||||
elements.push(hx`
|
||||
<i.icon.device
|
||||
<i
|
||||
class='icon device float-right'
|
||||
class=${dlnaClass}
|
||||
onclick=${dlnaHandler}>
|
||||
tv
|
||||
@@ -392,54 +443,71 @@ function renderPlayerControls (state) {
|
||||
`)
|
||||
}
|
||||
|
||||
// On OSX, the back button is in the title bar of the window; see app.js
|
||||
// On other platforms, we render one over the video on mouseover
|
||||
if (process.platform !== 'darwin') {
|
||||
elements.push(hx`
|
||||
<i.icon.back
|
||||
onclick=${dispatcher('back')}>
|
||||
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))'
|
||||
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>
|
||||
<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}
|
||||
style=${volumeStyle}
|
||||
/>
|
||||
<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>
|
||||
`)
|
||||
|
||||
// Finally, the big button in the center plays or pauses the video
|
||||
// Show video playback progress
|
||||
var currentTimeStr = formatTime(state.playing.currentTime)
|
||||
var durationStr = formatTime(state.playing.duration)
|
||||
elements.push(hx`
|
||||
<i class='icon play-pause' onclick=${dispatcher('playPause')}>
|
||||
${state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||
</i>
|
||||
<span class='time float-left'>
|
||||
${currentTimeStr} / ${durationStr}
|
||||
</span>
|
||||
`)
|
||||
|
||||
// render playback rate
|
||||
if (state.playing.playbackRate !== 1) {
|
||||
elements.push(hx`
|
||||
<span class='rate float-left'>
|
||||
${state.playing.playbackRate}x
|
||||
</span>
|
||||
`)
|
||||
}
|
||||
|
||||
return hx`
|
||||
<div class='player-controls'>
|
||||
<div class='controls'>
|
||||
${elements}
|
||||
${renderSubtitlesOptions(state)}
|
||||
</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)
|
||||
function handleScrub (e) {
|
||||
dispatch('mediaMouseMoved')
|
||||
@@ -540,3 +608,18 @@ function cssBackgroundImageDarkGradient () {
|
||||
return 'radial-gradient(circle at center, ' +
|
||||
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
||||
}
|
||||
|
||||
function formatTime (time) {
|
||||
if (typeof time !== 'number' || Number.isNaN(time)) {
|
||||
return '0:00'
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
102
renderer/views/preferences.js
Normal file
102
renderer/views/preferences.js
Normal file
@@ -0,0 +1,102 @@
|
||||
module.exports = Preferences
|
||||
|
||||
var hx = require('../lib/hx')
|
||||
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)
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
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')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function UnsupportedMediaModal (state) {
|
||||
var err = state.modal.error
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
module.exports = UpdateAvailableModal
|
||||
|
||||
var h = require('virtual-dom/h')
|
||||
var hyperx = require('hyperx')
|
||||
var hx = hyperx(h)
|
||||
|
||||
var electron = require('electron')
|
||||
|
||||
var {dispatch} = require('../lib/dispatcher')
|
||||
var hx = require('../lib/hx')
|
||||
|
||||
function UpdateAvailableModal (state) {
|
||||
return hx`
|
||||
|
||||
@@ -28,20 +28,13 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
||||
|
||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||
// client, as explained here: https://webtorrent.io/faq
|
||||
var client = window.client = new WebTorrent({
|
||||
tracker: {
|
||||
// HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome bug.
|
||||
// Fixed in Chrome 51, so we can remove this hack once Electron updates Chrome.
|
||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/353
|
||||
wrtc: process.platform !== 'darwin'
|
||||
}
|
||||
})
|
||||
var client = new WebTorrent()
|
||||
|
||||
// WebTorrent-to-HTTP streaming sever
|
||||
var server = window.server = null
|
||||
var server = null
|
||||
|
||||
// Used for diffing, so we only send progress updates when necessary
|
||||
var prevProgress = window.prevProgress = null
|
||||
var prevProgress = null
|
||||
|
||||
init()
|
||||
|
||||
@@ -277,7 +270,7 @@ function getTorrentProgress () {
|
||||
function startServer (infoHash, index) {
|
||||
var torrent = client.get(infoHash)
|
||||
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
|
||||
else torrent.on('ready', () => startServerFromReadyTorrent(torrent, index))
|
||||
else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
|
||||
}
|
||||
|
||||
function startServerFromReadyTorrent (torrent, index, cb) {
|
||||
@@ -349,10 +342,6 @@ function selectFiles (torrentOrInfoHash, selections) {
|
||||
} else {
|
||||
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
|
||||
file.deselect()
|
||||
|
||||
// If we deselected a file, try to nuke it to save disk space
|
||||
var filePath = path.join(torrent.path, file.path)
|
||||
fs.unlink(filePath) // Ignore errors for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user