Compare commits
219 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
fe50f76619 | ||
|
|
973a366b94 | ||
|
|
b0116deb35 | ||
|
|
511382d384 | ||
|
|
cfb3a01239 | ||
|
|
736d575ab1 | ||
|
|
34a9508483 | ||
|
|
21ed8797c2 | ||
|
|
454491572a | ||
|
|
6518a1535c | ||
|
|
0095687bf5 | ||
|
|
d466ed085a | ||
|
|
eeda7c17c5 | ||
|
|
b89deb46db | ||
|
|
951a89c6c9 | ||
|
|
d4e6c84279 | ||
|
|
9731d85ca3 | ||
|
|
98f7ba8931 | ||
|
|
24c775608e | ||
|
|
f4eab12c3f | ||
|
|
8eeddeb4bc | ||
|
|
58f1594d9e | ||
|
|
c126ac0a84 | ||
|
|
6768be710e | ||
|
|
b63aa090dc | ||
|
|
05ef8be5bc | ||
|
|
1a09249bc3 | ||
|
|
803820dfca | ||
|
|
deb111bf62 | ||
|
|
7d64c7e308 | ||
|
|
ffb7183f51 | ||
|
|
20c6737aba | ||
|
|
959fb20b61 | ||
|
|
5d14c923fa | ||
|
|
5ffa7c4465 | ||
|
|
461744da5b | ||
|
|
6df33bc58b | ||
|
|
b5ae8f56cf | ||
|
|
2e0de52520 | ||
|
|
7b1ff0efc6 | ||
|
|
4002392b7f | ||
|
|
ee4b84fc11 | ||
|
|
90a0ce4a4d | ||
|
|
80faba8234 | ||
|
|
ac0574a473 | ||
|
|
792e3430f1 | ||
|
|
9e33be0ab1 | ||
|
|
c343c008ed | ||
|
|
6405be5144 | ||
|
|
db743daae5 | ||
|
|
290a25c393 | ||
|
|
6589e134b3 | ||
|
|
a2aa5e4271 | ||
|
|
205e2eb551 | ||
|
|
53209a9da3 | ||
|
|
2a23611c5f | ||
|
|
cb71913cbe | ||
|
|
836d7c6664 | ||
|
|
4cef9f2911 | ||
|
|
0913988d53 | ||
|
|
6468f82a7f | ||
|
|
fd0fc769b1 | ||
|
|
e5b648dfc6 | ||
|
|
7701c5f097 | ||
|
|
e5eddce868 | ||
|
|
72f917a744 | ||
|
|
0b82c83d44 | ||
|
|
602654cc1d | ||
|
|
350bed53a3 | ||
|
|
840754fb59 | ||
|
|
ed46583226 | ||
|
|
93252d430e | ||
|
|
bfd09a058e | ||
|
|
b1a7543d37 | ||
|
|
39195fe8c4 | ||
|
|
ea1c66b3fc | ||
|
|
f35eb73d50 | ||
|
|
c99af4718e | ||
|
|
dbef07e334 | ||
|
|
969ad64c47 | ||
|
|
5dd5e8661b | ||
|
|
5c9265fc99 | ||
|
|
1deab08d38 | ||
|
|
3d6da99e8e | ||
|
|
2005ee4d0b | ||
|
|
c99da2ccaa | ||
|
|
4bffb6634c | ||
|
|
504aca747d | ||
|
|
2085312c34 | ||
|
|
744d38259e | ||
|
|
868739445a | ||
|
|
98d8a798ce | ||
|
|
fe31cfaa3e | ||
|
|
17d5490448 | ||
|
|
d4c415d585 | ||
|
|
cb8f7f53c2 | ||
|
|
8d93641ebe | ||
|
|
4faf30e0a1 | ||
|
|
ed1b27ede0 | ||
|
|
252443a529 | ||
|
|
86f5a1a54e | ||
|
|
0b1872fa28 | ||
|
|
9eeb8133af | ||
|
|
1eb5504029 | ||
|
|
dfe8c3eb6b | ||
|
|
2b8c1fe709 | ||
|
|
905cc527d0 | ||
|
|
95019453fd | ||
|
|
e46a7f42df | ||
|
|
15a59f445b | ||
|
|
dea951fc42 | ||
|
|
347eb2c7f0 | ||
|
|
4221883eb4 | ||
|
|
27f729250f | ||
|
|
452bbb60c4 | ||
|
|
9d4aeaedd3 | ||
|
|
558b6c1648 | ||
|
|
98e263e69a | ||
|
|
18b126e0d2 | ||
|
|
82dff65572 | ||
|
|
d60d298b8f | ||
|
|
ffbd8184b5 | ||
|
|
11cf4aeecd | ||
|
|
b0b8b56816 | ||
|
|
967e5ecb9c | ||
|
|
f0315f7f77 | ||
|
|
facb07cbb1 | ||
|
|
41910aea9c | ||
|
|
8fcfa3b97a | ||
|
|
8ebb2349dd | ||
|
|
1e487a3c2a | ||
|
|
291ea94a10 | ||
|
|
ade6c1e4a0 | ||
|
|
bde5dc14c3 | ||
|
|
0a005eb054 | ||
|
|
735851486e | ||
|
|
56ba5c705a | ||
|
|
cdab2dbc65 | ||
|
|
4284eb8f75 | ||
|
|
2707fc9053 | ||
|
|
1d4d4319e4 | ||
|
|
c5cc0ce09d | ||
|
|
fdd7dab76f | ||
|
|
7624f2da98 | ||
|
|
ef51f827dc | ||
|
|
011ab13c83 | ||
|
|
017d61815f |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
10
AUTHORS.md
10
AUTHORS.md
@@ -11,5 +11,15 @@
|
|||||||
- Dan Flettre <fletd01@yahoo.com>
|
- Dan Flettre <fletd01@yahoo.com>
|
||||||
- Liam Gray <liam.r.gray@gmail.com>
|
- Liam Gray <liam.r.gray@gmail.com>
|
||||||
- grunjol <grunjol@argenteam.net>
|
- grunjol <grunjol@argenteam.net>
|
||||||
|
- Rémi Jouannet <remijouannet@users.noreply.github.com>
|
||||||
|
- Evan Miller <miller.evan815@gmail.com>
|
||||||
|
- Alex <alxmorais8@msn.com>
|
||||||
|
- Diego Rodríguez Baquero <diegorbaquero@gmail.com>
|
||||||
|
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
|
||||||
|
- gabriel <furstenheim@gmail.com>
|
||||||
|
- Rolando Guedes <rolando.guedes@3gnt.net>
|
||||||
|
- Benjamin Tan <demoneaux@gmail.com>
|
||||||
|
- Mathias Rasmussen <mathiasvr@gmail.com>
|
||||||
|
- Sergey Bargamon <sergey@bargamon.ru>
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
110
CHANGELOG.md
110
CHANGELOG.md
@@ -1,16 +1,118 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
## UNRELEASED
|
## v0.6.0 - 2016-05-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added Preferences page
|
||||||
|
- Save video position, resume playback from saved position
|
||||||
|
- Add additional video player keyboard shortcuts (#275)
|
||||||
|
- Use `poster.jpg` file as the poster image if available (#558)
|
||||||
|
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
|
||||||
|
- Add support for pasting a `instant.io` links (#559)
|
||||||
|
- Add announcement feature
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Use Squirrel.Windows 1.3.0
|
- Nicer player UI
|
||||||
- Fix installing when the app is already installed
|
- Reduce startup jank, improve startup time (#568)
|
||||||
- Don't kill unrelated processes on uninstall
|
- Cleanup unsupported codec detection (#569, #570)
|
||||||
|
- Cleaner look for the torrent file list
|
||||||
|
- Improve subtitle positioning (#551)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
|
||||||
|
- Fix bugs in LocationHistory
|
||||||
|
- When player is active, and magnet link is pasted, go back to list
|
||||||
|
- After deleting torrent, remove just the player from forward stack
|
||||||
|
- After creating torrent, remove create torrent page from forward stack
|
||||||
|
- Cancel button on create torrent page should only go back one page
|
||||||
|
|
||||||
|
## v0.5.1 - 2016-05-18
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix auto-updater (OS X, Windows).
|
||||||
|
|
||||||
|
## v0.5.0 - 2016-05-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Select/deselect individual files to torrent.
|
||||||
|
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
|
||||||
|
- "Add Subtitle File..." menu item.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- When manually adding subtitle track(s), always switch to the new track.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Magnet links throw exception on app launch. (OS X)
|
||||||
|
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
|
||||||
|
- Missing 'About WebTorrent' menu item. (Windows)
|
||||||
|
- Rare exception. ("Cannot create BrowserWindow before app is ready")
|
||||||
|
|
||||||
|
## v0.4.0 - 2016-05-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Better Windows support!
|
||||||
|
- Windows 32-bit build.
|
||||||
|
- Windows Portable App build.
|
||||||
|
- Windows app signing, for fewer install warnings.
|
||||||
|
- Better Linux support!
|
||||||
|
- Linux 32-bit build.
|
||||||
|
- Subtitles support!
|
||||||
|
- .srt and .vtt file support.
|
||||||
|
- Drag-and-drop files on video, or choose from file selector.
|
||||||
|
- Multiple subtitle files support.
|
||||||
|
- Stream to VLC when the audio codec is unplayable (e.g. AC3, EAC3).
|
||||||
|
- "Show in Folder" item in context menu.
|
||||||
|
- Volume slider, with mute/unmute button.
|
||||||
|
- New "Create torrent" page to modify:
|
||||||
|
- Torrent comment.
|
||||||
|
- Trackers.
|
||||||
|
- Private torrent flag.
|
||||||
|
- Use mouse wheel to increase/decrease volume.
|
||||||
|
- Bounce the Downloads stack when download completes. (OS X)
|
||||||
|
- New default torrent on first launch: The WIRED CD.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improve app startup time by 40%.
|
||||||
|
- UI tweaks: Reduce font size, reduce torrent list item height.
|
||||||
|
- Add Playback menu for playback-related functionality.
|
||||||
|
- Fix installing when the app is already installed. (Windows)
|
||||||
|
- Don't kill unrelated processes on uninstall. (Windows)
|
||||||
|
- Set "sheet offset" correctly for create torrent dialog. (OS X)
|
||||||
|
- Remove OS X-style Window menu. (Linux, Windows)
|
||||||
|
- Remove "Add Fake Airplay/Chromecast" menu items.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is
|
||||||
|
temporary. (OS X)
|
||||||
|
- When fullscreen, make controls use the full window. (OS X)
|
||||||
|
- Support creating torrents that contain .torrent files.
|
||||||
|
- Block power save while casting to a remote device.
|
||||||
|
- Do not block power save when the space key is pressed from the torrent list.
|
||||||
|
- Support playing .mpg and .ogv extensions in the app.
|
||||||
|
- Fix video centering for multi-screen setups.
|
||||||
|
- Show an error when adding a duplicate torrent.
|
||||||
|
- Show an error when adding an invalid magnet link.
|
||||||
|
- Do not stop music when tabbing to another program (OS X)
|
||||||
|
- Properly size the Windows volume mixer icon.
|
||||||
|
- Default to the user's OS-defined, localized "Downloads" folder.
|
||||||
|
- Enforce minimimum window size when resizing player to prevent window disappearing.
|
||||||
|
- Fix rare race condition error on app quit.
|
||||||
|
- Don't use zero-byte torrent "poster" images.
|
||||||
|
|
||||||
|
Thanks to @grunjol, @rguedes, @furstenheim, @karloluis, @DiegoRBaquero, @alxhotel,
|
||||||
|
@AgentEpsilon, @remijouannet, Rolando Guedes, @dcposch, and @feross for contributing
|
||||||
|
to this release!
|
||||||
|
|
||||||
## v0.3.3 - 2016-04-07
|
## v0.3.3 - 2016-04-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -87,4 +87,4 @@ brew install wine
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org).
|
MIT. Copyright (c) [WebTorrent, LLC](https://webtorrent.io).
|
||||||
|
|||||||
51
bin/check-deps.js
Executable file
51
bin/check-deps.js
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
var fs = require('fs')
|
||||||
|
var cp = require('child_process')
|
||||||
|
|
||||||
|
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen']
|
||||||
|
var EXECUTABLE_DEPS = ['gh-release', 'standard']
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
// Scans our codebase and package.json for missing or unused dependencies
|
||||||
|
// Process returns 0 on success, prints a message and returns 1 on failure
|
||||||
|
function main () {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
console.log('Sorry, check-deps only works on Mac and Linux')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsDeps = findJSDeps()
|
||||||
|
var packageDeps = findPackageDeps()
|
||||||
|
|
||||||
|
var missingDeps = jsDeps.filter((dep) =>
|
||||||
|
packageDeps.indexOf(dep) < 0 &&
|
||||||
|
BUILT_IN_DEPS.indexOf(dep) < 0)
|
||||||
|
var unusedDeps = packageDeps.filter((dep) =>
|
||||||
|
jsDeps.indexOf(dep) < 0 &&
|
||||||
|
EXECUTABLE_DEPS.indexOf(dep) < 0)
|
||||||
|
|
||||||
|
if (missingDeps.length > 0) console.log('Missing package dependencies: ' + missingDeps)
|
||||||
|
if (unusedDeps.length > 0) console.log('Unused package dependencies: ' + unusedDeps)
|
||||||
|
|
||||||
|
if (missingDeps.length + unusedDeps.length > 0) process.exit(1)
|
||||||
|
|
||||||
|
console.log('Lookin good!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds all dependencies, required, optional, or dev, in package.json
|
||||||
|
function findPackageDeps () {
|
||||||
|
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||||
|
var requiredDeps = Object.keys(pkg.dependencies)
|
||||||
|
var devDeps = Object.keys(pkg.devDependencies)
|
||||||
|
var optionalDeps = Object.keys(pkg.optionalDependencies)
|
||||||
|
|
||||||
|
return [].concat(requiredDeps, devDeps, optionalDeps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds all dependencies required() in the code
|
||||||
|
function findJSDeps () {
|
||||||
|
var stdout = cp.execSync('./bin/list-deps.sh')
|
||||||
|
return stdout.toString().trim().split('\n')
|
||||||
|
}
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
* Useful for developers.
|
* Useful for developers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var fs = require('fs')
|
||||||
var os = require('os')
|
var os = require('os')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var pathExists = require('path-exists')
|
|
||||||
var rimraf = require('rimraf')
|
var rimraf = require('rimraf')
|
||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
@@ -15,7 +15,12 @@ var handlers = require('../main/handlers')
|
|||||||
|
|
||||||
rimraf.sync(config.CONFIG_PATH)
|
rimraf.sync(config.CONFIG_PATH)
|
||||||
|
|
||||||
var tmpPath = path.join(pathExists.sync('/tmp') ? '/tmp' : os.tmpDir(), 'webtorrent')
|
var tmpPath
|
||||||
|
try {
|
||||||
|
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
|
||||||
|
} catch (err) {
|
||||||
|
tmpPath = path.join(os.tmpDir(), 'webtorrent')
|
||||||
|
}
|
||||||
rimraf.sync(tmpPath)
|
rimraf.sync(tmpPath)
|
||||||
|
|
||||||
// Uninstall .torrent file and magnet link handlers
|
// Uninstall .torrent file and magnet link handlers
|
||||||
|
|||||||
10
bin/list-deps.sh
Executable file
10
bin/list-deps.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# This is a truly heinous hack, but it works pretty nicely.
|
||||||
|
# Find all modules we're requiring---even conditional requires.
|
||||||
|
|
||||||
|
grep "require('" *.js bin/ main/ renderer/ -R |
|
||||||
|
grep '.js:' |
|
||||||
|
sed "s/.*require('\([^'\/]*\).*/\1/" |
|
||||||
|
grep -v '^\.' |
|
||||||
|
sort |
|
||||||
|
uniq
|
||||||
8
bin/open-config.js
Executable file
8
bin/open-config.js
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
var open = require('open')
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
|
var configPath = path.join(config.CONFIG_PATH, 'config.json')
|
||||||
|
open(configPath)
|
||||||
@@ -9,6 +9,7 @@ var electronPackager = require('electron-packager')
|
|||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var minimist = require('minimist')
|
var minimist = require('minimist')
|
||||||
var mkdirp = require('mkdirp')
|
var mkdirp = require('mkdirp')
|
||||||
|
var os = require('os')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var rimraf = require('rimraf')
|
var rimraf = require('rimraf')
|
||||||
var series = require('run-series')
|
var series = require('run-series')
|
||||||
@@ -18,16 +19,6 @@ var config = require('../config')
|
|||||||
var pkg = require('../package.json')
|
var pkg = require('../package.json')
|
||||||
|
|
||||||
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
||||||
|
|
||||||
/*
|
|
||||||
* Path to folder with the following files:
|
|
||||||
* - Windows Authenticode private key and cert (authenticode.p12)
|
|
||||||
* - Windows Authenticode password file (authenticode.txt)
|
|
||||||
*/
|
|
||||||
var CERT_PATH = process.platform === 'win32'
|
|
||||||
? 'D:'
|
|
||||||
: '/Volumes/Certs'
|
|
||||||
|
|
||||||
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
||||||
|
|
||||||
var argv = minimist(process.argv.slice(2), {
|
var argv = minimist(process.argv.slice(2), {
|
||||||
@@ -64,9 +55,6 @@ function build () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var all = {
|
var all = {
|
||||||
// Build 64 bit binaries only.
|
|
||||||
arch: 'x64',
|
|
||||||
|
|
||||||
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
|
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
|
||||||
// property on Windows, and `NSHumanReadableCopyright` on OS X.
|
// property on Windows, and `NSHumanReadableCopyright` on OS X.
|
||||||
'app-copyright': config.APP_COPYRIGHT,
|
'app-copyright': config.APP_COPYRIGHT,
|
||||||
@@ -85,9 +73,9 @@ var all = {
|
|||||||
'asar-unpack': 'WebTorrent*',
|
'asar-unpack': 'WebTorrent*',
|
||||||
|
|
||||||
// The build version of the application. Maps to the FileVersion metadata property on
|
// The build version of the application. Maps to the FileVersion metadata property on
|
||||||
// Windows, and CFBundleVersion on OS X. We're using the short git hash (e.g. 'e7d837e')
|
// Windows, and CFBundleVersion on OS X. Note: Windows requires the build version to
|
||||||
// Windows requires the build version to start with a number :/ so we stick on a prefix
|
// start with a number. We're using the version of the underlying WebTorrent library.
|
||||||
'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''),
|
'build-version': require('webtorrent/package.json').version,
|
||||||
|
|
||||||
// The application source directory.
|
// The application source directory.
|
||||||
dir: config.ROOT_PATH,
|
dir: config.ROOT_PATH,
|
||||||
@@ -110,12 +98,16 @@ var all = {
|
|||||||
prune: true,
|
prune: true,
|
||||||
|
|
||||||
// The Electron version with which the app is built (without the leading 'v')
|
// The Electron version with which the app is built (without the leading 'v')
|
||||||
version: pkg.dependencies['electron-prebuilt']
|
version: require('electron-prebuilt/package.json').version
|
||||||
}
|
}
|
||||||
|
|
||||||
var darwin = {
|
var darwin = {
|
||||||
|
// Build for OS X
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
|
|
||||||
|
// Build 64 bit binaries only.
|
||||||
|
arch: 'x64',
|
||||||
|
|
||||||
// The bundle identifier to use in the application's plist (OS X only).
|
// The bundle identifier to use in the application's plist (OS X only).
|
||||||
'app-bundle-id': 'io.webtorrent.webtorrent',
|
'app-bundle-id': 'io.webtorrent.webtorrent',
|
||||||
|
|
||||||
@@ -131,8 +123,12 @@ var darwin = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var win32 = {
|
var win32 = {
|
||||||
|
// Build for Windows.
|
||||||
platform: 'win32',
|
platform: 'win32',
|
||||||
|
|
||||||
|
// Build 32 bit binaries only.
|
||||||
|
arch: 'ia32',
|
||||||
|
|
||||||
// Object hash of application metadata to embed into the executable (Windows only)
|
// Object hash of application metadata to embed into the executable (Windows only)
|
||||||
'version-string': {
|
'version-string': {
|
||||||
|
|
||||||
@@ -161,9 +157,10 @@ var win32 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var linux = {
|
var linux = {
|
||||||
|
// Build for Linux.
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
|
|
||||||
// Build 32/64 bit binaries.
|
// Build 32 and 64 bit binaries.
|
||||||
arch: 'all'
|
arch: 'all'
|
||||||
|
|
||||||
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
||||||
@@ -177,7 +174,7 @@ function buildDarwin (cb) {
|
|||||||
console.log('OS X: Packaging electron...')
|
console.log('OS X: Packaging electron...')
|
||||||
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log('OS X: Packaged electron. ' + buildPath[0])
|
console.log('OS X: Packaged electron. ' + buildPath)
|
||||||
|
|
||||||
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||||
var contentsPath = path.join(appPath, 'Contents')
|
var contentsPath = path.join(appPath, 'Contents')
|
||||||
@@ -185,8 +182,6 @@ function buildDarwin (cb) {
|
|||||||
var infoPlistPath = path.join(contentsPath, 'Info.plist')
|
var infoPlistPath = path.join(contentsPath, 'Info.plist')
|
||||||
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
|
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
|
||||||
|
|
||||||
// TODO: Use new `extend-info` and `extra-resource` opts to electron-packager,
|
|
||||||
// available as of v6.
|
|
||||||
infoPlist.CFBundleDocumentTypes = [
|
infoPlist.CFBundleDocumentTypes = [
|
||||||
{
|
{
|
||||||
CFBundleTypeExtensions: [ 'torrent' ],
|
CFBundleTypeExtensions: [ 'torrent' ],
|
||||||
@@ -214,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))
|
fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
|
||||||
|
|
||||||
// Copy torrent file icon into app bundle
|
// Copy torrent file icon into app bundle
|
||||||
@@ -277,7 +291,7 @@ function buildDarwin (cb) {
|
|||||||
|
|
||||||
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
|
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
|
||||||
zip(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log('OS X: Created zip.')
|
console.log('OS X: Created zip.')
|
||||||
}
|
}
|
||||||
@@ -327,11 +341,24 @@ function buildDarwin (cb) {
|
|||||||
|
|
||||||
function buildWin32 (cb) {
|
function buildWin32 (cb) {
|
||||||
var installer = require('electron-winstaller')
|
var installer = require('electron-winstaller')
|
||||||
|
|
||||||
console.log('Windows: Packaging electron...')
|
console.log('Windows: Packaging electron...')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path to folder with the following files:
|
||||||
|
* - Windows Authenticode private key and cert (authenticode.p12)
|
||||||
|
* - Windows Authenticode password file (authenticode.txt)
|
||||||
|
*/
|
||||||
|
var CERT_PATH
|
||||||
|
try {
|
||||||
|
fs.accessSync('D:')
|
||||||
|
CERT_PATH = 'D:'
|
||||||
|
} catch (err) {
|
||||||
|
CERT_PATH = path.join(os.homedir(), 'Desktop')
|
||||||
|
}
|
||||||
|
|
||||||
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log('Windows: Packaged electron. ' + buildPath[0])
|
console.log('Windows: Packaged electron. ' + buildPath)
|
||||||
|
|
||||||
var signWithParams
|
var signWithParams
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
@@ -358,6 +385,7 @@ function buildWin32 (cb) {
|
|||||||
|
|
||||||
function packageInstaller (cb) {
|
function packageInstaller (cb) {
|
||||||
console.log('Windows: Creating installer...')
|
console.log('Windows: Creating installer...')
|
||||||
|
|
||||||
installer.createWindowsInstaller({
|
installer.createWindowsInstaller({
|
||||||
appDirectory: buildPath[0],
|
appDirectory: buildPath[0],
|
||||||
authors: config.APP_TEAM,
|
authors: config.APP_TEAM,
|
||||||
@@ -376,14 +404,15 @@ function buildWin32 (cb) {
|
|||||||
title: config.APP_NAME,
|
title: config.APP_NAME,
|
||||||
usePackageJson: false,
|
usePackageJson: false,
|
||||||
version: pkg.version
|
version: pkg.version
|
||||||
}).then(function () {
|
})
|
||||||
|
.then(function () {
|
||||||
console.log('Windows: Created installer.')
|
console.log('Windows: Created installer.')
|
||||||
cb(null)
|
cb(null)
|
||||||
}).catch(cb)
|
})
|
||||||
|
.catch(cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
function packagePortable (cb) {
|
function packagePortable (cb) {
|
||||||
// Create Windows portable app
|
|
||||||
console.log('Windows: Creating portable app...')
|
console.log('Windows: Creating portable app...')
|
||||||
|
|
||||||
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
||||||
@@ -391,7 +420,7 @@ function buildWin32 (cb) {
|
|||||||
|
|
||||||
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
||||||
zip(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log('Windows: Created portable app.')
|
console.log('Windows: Created portable app.')
|
||||||
cb(null)
|
cb(null)
|
||||||
@@ -403,7 +432,7 @@ function buildLinux (cb) {
|
|||||||
console.log('Linux: Packaging electron...')
|
console.log('Linux: Packaging electron...')
|
||||||
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log('Linux: Packaged electron. ' + buildPath[0])
|
console.log('Linux: Packaged electron. ' + buildPath)
|
||||||
|
|
||||||
var tasks = []
|
var tasks = []
|
||||||
buildPath.forEach(function (filesPath) {
|
buildPath.forEach(function (filesPath) {
|
||||||
@@ -455,7 +484,7 @@ function buildLinux (cb) {
|
|||||||
|
|
||||||
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
|
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
|
||||||
zip(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log(`Linux: Created ${destArch} zip.`)
|
console.log(`Linux: Created ${destArch} zip.`)
|
||||||
cb(null)
|
cb(null)
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ npm run package -- --sign
|
|||||||
git push
|
git push
|
||||||
git push --tags
|
git push --tags
|
||||||
npm publish
|
npm publish
|
||||||
gh-release
|
./node_modules/.bin/gh-release
|
||||||
|
|||||||
@@ -6,6 +6,5 @@ npm run update-authors
|
|||||||
git diff --exit-code
|
git diff --exit-code
|
||||||
rm -rf node_modules/
|
rm -rf node_modules/
|
||||||
npm install
|
npm install
|
||||||
npm prune
|
|
||||||
npm dedupe
|
npm dedupe
|
||||||
npm test
|
npm test
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ while (<>) {
|
|||||||
next if /<support\@greenkeeper.io>/;
|
next if /<support\@greenkeeper.io>/;
|
||||||
next if /<ungoldman\@gmail.com>/;
|
next if /<ungoldman\@gmail.com>/;
|
||||||
next if /<grunjol\@users.noreply.github.com>/;
|
next if /<grunjol\@users.noreply.github.com>/;
|
||||||
|
next if /<dc\@DCs-MacBook.local>/;
|
||||||
|
next if /<rolandoguedes\@gmail.com>/;
|
||||||
$seen{$_} = push @authors, "- ", $_;
|
$seen{$_} = push @authors, "- ", $_;
|
||||||
}
|
}
|
||||||
END {
|
END {
|
||||||
|
|||||||
23
config.js
23
config.js
@@ -1,14 +1,16 @@
|
|||||||
var appConfig = require('application-config')('WebTorrent')
|
var appConfig = require('application-config')('WebTorrent')
|
||||||
|
var fs = require('fs')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var pathExists = require('path-exists')
|
|
||||||
|
|
||||||
var APP_NAME = 'WebTorrent'
|
var APP_NAME = 'WebTorrent'
|
||||||
var APP_TEAM = 'The WebTorrent Project'
|
var APP_TEAM = 'WebTorrent, LLC'
|
||||||
var APP_VERSION = require('./package.json').version
|
var APP_VERSION = require('./package.json').version
|
||||||
|
|
||||||
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||||
|
|
||||||
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
||||||
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
||||||
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
|
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
|
||||||
@@ -17,9 +19,7 @@ module.exports = {
|
|||||||
APP_VERSION: APP_VERSION,
|
APP_VERSION: APP_VERSION,
|
||||||
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
||||||
|
|
||||||
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
|
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
|
||||||
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' +
|
|
||||||
'?version=' + APP_VERSION + '&platform=' + process.platform,
|
|
||||||
|
|
||||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||||
|
|
||||||
@@ -27,9 +27,14 @@ module.exports = {
|
|||||||
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||||
|
|
||||||
|
DELAYED_INIT: 3000 /* 3 seconds */,
|
||||||
|
|
||||||
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
||||||
|
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
||||||
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
||||||
|
|
||||||
|
HOME_PAGE_URL: 'https://webtorrent.io',
|
||||||
|
|
||||||
IS_PORTABLE: isPortable(),
|
IS_PORTABLE: isPortable(),
|
||||||
IS_PRODUCTION: isProduction(),
|
IS_PRODUCTION: isProduction(),
|
||||||
|
|
||||||
@@ -53,7 +58,11 @@ function getConfigPath () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPortable () {
|
function isPortable () {
|
||||||
return process.platform === 'win32' && isProduction() && pathExists(PORTABLE_PATH)
|
try {
|
||||||
|
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProduction () {
|
function isProduction () {
|
||||||
@@ -61,7 +70,7 @@ function isProduction () {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
return !/\/Electron\.app\/Contents\/MacOS\/Electron$/.test(process.execPath)
|
return !/\/Electron\.app\//.test(process.execPath)
|
||||||
}
|
}
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
return !/\\electron\.exe$/.test(process.execPath)
|
return !/\\electron\.exe$/.test(process.execPath)
|
||||||
|
|||||||
@@ -11,5 +11,4 @@ function init () {
|
|||||||
productName: config.APP_NAME,
|
productName: config.APP_NAME,
|
||||||
submitURL: config.CRASH_REPORT_URL
|
submitURL: config.CRASH_REPORT_URL
|
||||||
})
|
})
|
||||||
console.log('crash reporter started')
|
|
||||||
}
|
}
|
||||||
|
|||||||
38
main/announcement.js
Normal file
38
main/announcement.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
module.exports = {
|
||||||
|
init
|
||||||
|
}
|
||||||
|
|
||||||
|
var electron = require('electron')
|
||||||
|
var get = require('simple-get')
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
var log = require('./log')
|
||||||
|
|
||||||
|
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
||||||
|
'?version=' + config.APP_VERSION +
|
||||||
|
'&platform=' + process.platform
|
||||||
|
|
||||||
|
function init () {
|
||||||
|
get.concat(ANNOUNCEMENT_URL, function (err, res, data) {
|
||||||
|
if (err) return log('failed to retrieve remote message')
|
||||||
|
if (res.statusCode !== 200) return log('no remote message')
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data.toString())
|
||||||
|
} catch (err) {
|
||||||
|
data = {
|
||||||
|
title: 'WebTorrent Desktop Announcement',
|
||||||
|
message: 'WebTorrent Desktop Announcement',
|
||||||
|
detail: data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
buttons: ['OK'],
|
||||||
|
title: data.title,
|
||||||
|
message: data.message,
|
||||||
|
detail: data.detail
|
||||||
|
}, function () {})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
init
|
|
||||||
}
|
|
||||||
|
|
||||||
var electron = require('electron')
|
|
||||||
var get = require('simple-get')
|
|
||||||
|
|
||||||
var config = require('../config')
|
|
||||||
var log = require('./log')
|
|
||||||
var windows = require('./windows')
|
|
||||||
|
|
||||||
var autoUpdater = electron.autoUpdater
|
|
||||||
|
|
||||||
function init () {
|
|
||||||
autoUpdater.on('error', function (err) {
|
|
||||||
log.error('App update error: ' + err.message || err)
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.setFeedURL(config.AUTO_UPDATE_URL)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* We always check for updates on app startup. To keep app startup fast, we delay this
|
|
||||||
* first check so it happens when there is less going on.
|
|
||||||
*/
|
|
||||||
setTimeout(checkForUpdates, config.AUTO_UPDATE_CHECK_STARTUP_DELAY)
|
|
||||||
|
|
||||||
autoUpdater.on('checking-for-update', () => log('Checking for app update'))
|
|
||||||
autoUpdater.on('update-available', () => log('App update available'))
|
|
||||||
autoUpdater.on('update-not-available', () => log('App update not available'))
|
|
||||||
autoUpdater.on('update-downloaded', function (e, releaseNotes, releaseName, releaseDate, updateURL) {
|
|
||||||
log('App update downloaded: ', releaseName, updateURL)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkForUpdates () {
|
|
||||||
// Electron's built-in auto updater only supports Mac and Windows, for now
|
|
||||||
if (process.platform !== 'linux') {
|
|
||||||
return autoUpdater.checkForUpdates()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're on Linux, we have to do it ourselves
|
|
||||||
get.concat(config.AUTO_UPDATE_URL, function (err, res, data) {
|
|
||||||
if (err) return log('Error checking for app update: ' + err.message)
|
|
||||||
if (![200, 204].includes(res.statusCode)) return log('Error checking for app update, got HTTP ' + res.statusCode)
|
|
||||||
if (res.statusCode !== 200) return
|
|
||||||
|
|
||||||
var obj = JSON.parse(data)
|
|
||||||
windows.main.send('dispatch', 'updateAvailable', obj.version)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@ module.exports = {
|
|||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
|
||||||
function install () {
|
function install () {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
installDarwin()
|
installDarwin()
|
||||||
@@ -42,6 +44,12 @@ function installDarwin () {
|
|||||||
|
|
||||||
function uninstallDarwin () {}
|
function uninstallDarwin () {}
|
||||||
|
|
||||||
|
var EXEC_COMMAND = [ process.execPath ]
|
||||||
|
|
||||||
|
if (!config.IS_PRODUCTION) {
|
||||||
|
EXEC_COMMAND.push(config.ROOT_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
function installWin32 () {
|
function installWin32 () {
|
||||||
var Registry = require('winreg')
|
var Registry = require('winreg')
|
||||||
|
|
||||||
@@ -49,8 +57,8 @@ function installWin32 () {
|
|||||||
|
|
||||||
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
||||||
|
|
||||||
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, process.execPath)
|
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
|
||||||
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, process.execPath)
|
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:
|
* To add a protocol handler, the following keys must be added to the Windows registry:
|
||||||
@@ -108,7 +116,7 @@ function installWin32 () {
|
|||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
||||||
})
|
})
|
||||||
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done)
|
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
|
||||||
}
|
}
|
||||||
|
|
||||||
function done (err) {
|
function done (err) {
|
||||||
@@ -169,7 +177,7 @@ function installWin32 () {
|
|||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
|
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
|
||||||
})
|
})
|
||||||
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done)
|
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
|
||||||
}
|
}
|
||||||
|
|
||||||
function done (err) {
|
function done (err) {
|
||||||
@@ -181,8 +189,8 @@ function installWin32 () {
|
|||||||
function uninstallWin32 () {
|
function uninstallWin32 () {
|
||||||
var Registry = require('winreg')
|
var Registry = require('winreg')
|
||||||
|
|
||||||
unregisterProtocolHandlerWin32('magnet', process.execPath)
|
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
||||||
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', process.execPath)
|
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
|
||||||
|
|
||||||
function unregisterProtocolHandlerWin32 (protocol, command) {
|
function unregisterProtocolHandlerWin32 (protocol, command) {
|
||||||
getCommand()
|
getCommand()
|
||||||
@@ -193,7 +201,7 @@ function uninstallWin32 () {
|
|||||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
||||||
})
|
})
|
||||||
commandKey.get('', function (err, item) {
|
commandKey.get('', function (err, item) {
|
||||||
if (!err && item.value.indexOf(command) >= 0) {
|
if (!err && item.value.indexOf(commandToArgs(command)) >= 0) {
|
||||||
destroyProtocol()
|
destroyProtocol()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -241,6 +249,10 @@ function uninstallWin32 () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commandToArgs (command) {
|
||||||
|
return command.map((arg) => `"${arg}"`).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
function installLinux () {
|
function installLinux () {
|
||||||
var fs = require('fs-extra')
|
var fs = require('fs-extra')
|
||||||
var os = require('os')
|
var os = require('os')
|
||||||
@@ -260,14 +272,14 @@ function installLinux () {
|
|||||||
function writeDesktopFile (err, desktopFile) {
|
function writeDesktopFile (err, desktopFile) {
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var appPath = config.IS_PRODUCTION ? path.dirname(process.execPath) : config.ROOT_PATH
|
var appPath = config.IS_PRODUCTION
|
||||||
var execPath = process.execPath + (config.IS_PRODUCTION ? '' : ' \.')
|
? path.dirname(process.execPath)
|
||||||
var tryExecPath = process.execPath
|
: config.ROOT_PATH
|
||||||
|
|
||||||
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
|
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
|
||||||
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
|
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
|
||||||
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, execPath)
|
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
|
||||||
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, tryExecPath)
|
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
|
||||||
|
|
||||||
var desktopFilePath = path.join(
|
var desktopFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
|
console.time('init')
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
var ipcMain = electron.ipcMain
|
var ipcMain = electron.ipcMain
|
||||||
|
|
||||||
var autoUpdater = require('./auto-updater')
|
var announcement = require('./announcement')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var crashReporter = require('../crash-reporter')
|
var crashReporter = require('../crash-reporter')
|
||||||
var handlers = require('./handlers')
|
var handlers = require('./handlers')
|
||||||
var ipc = require('./ipc')
|
var ipc = require('./ipc')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var shortcuts = require('./shortcuts')
|
|
||||||
var squirrelWin32 = require('./squirrel-win32')
|
var squirrelWin32 = require('./squirrel-win32')
|
||||||
var windows = require('./windows')
|
|
||||||
var tray = require('./tray')
|
var tray = require('./tray')
|
||||||
|
var updater = require('./updater')
|
||||||
|
var windows = require('./windows')
|
||||||
|
|
||||||
var shouldQuit = false
|
var shouldQuit = false
|
||||||
var argv = sliceArgv(process.argv)
|
var argv = sliceArgv(process.argv)
|
||||||
@@ -41,6 +43,7 @@ function init () {
|
|||||||
app.setPath('userData', config.CONFIG_PATH)
|
app.setPath('userData', config.CONFIG_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isReady = false // app ready, windows can be created
|
||||||
app.ipcReady = false // main window has finished loading and IPC is ready
|
app.ipcReady = false // main window has finished loading and IPC is ready
|
||||||
app.isQuitting = false
|
app.isQuitting = false
|
||||||
|
|
||||||
@@ -52,21 +55,23 @@ function init () {
|
|||||||
|
|
||||||
app.on('will-finish-launching', function () {
|
app.on('will-finish-launching', function () {
|
||||||
crashReporter.init()
|
crashReporter.init()
|
||||||
autoUpdater.init()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('ready', function () {
|
app.on('ready', function () {
|
||||||
menu.init()
|
isReady = true
|
||||||
|
|
||||||
windows.createMainWindow()
|
windows.createMainWindow()
|
||||||
windows.createWebTorrentHiddenWindow()
|
windows.createWebTorrentHiddenWindow()
|
||||||
shortcuts.init()
|
menu.init()
|
||||||
tray.init()
|
|
||||||
handlers.install()
|
// To keep app startup fast, some code is delayed.
|
||||||
|
setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('ipcReady', function () {
|
app.on('ipcReady', function () {
|
||||||
log('Command line args:', argv)
|
log('Command line args:', argv)
|
||||||
processArgv(argv)
|
processArgv(argv)
|
||||||
|
console.timeEnd('init')
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('before-quit', function (e) {
|
app.on('before-quit', function (e) {
|
||||||
@@ -80,10 +85,17 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
windows.createMainWindow()
|
if (isReady) windows.createMainWindow()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delayedInit () {
|
||||||
|
announcement.init()
|
||||||
|
tray.init()
|
||||||
|
handlers.install()
|
||||||
|
updater.init()
|
||||||
|
}
|
||||||
|
|
||||||
function onOpen (e, torrentId) {
|
function onOpen (e, torrentId) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -118,18 +130,27 @@ function sliceArgv (argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function processArgv (argv) {
|
function processArgv (argv) {
|
||||||
|
var pathsToOpen = []
|
||||||
argv.forEach(function (arg) {
|
argv.forEach(function (arg) {
|
||||||
if (arg === '-n') {
|
if (arg === '-n') {
|
||||||
windows.main.send('dispatch', 'showOpenSeedFiles')
|
menu.showOpenSeedFiles()
|
||||||
} else if (arg === '-o') {
|
} else if (arg === '-o') {
|
||||||
windows.main.send('dispatch', 'showOpenTorrentFile')
|
menu.showOpenTorrentFile()
|
||||||
} else if (arg === '-u') {
|
} else if (arg === '-u') {
|
||||||
windows.main.send('showOpenTorrentAddress')
|
menu.showOpenTorrentAddress()
|
||||||
} else if (arg.startsWith('-psn')) {
|
} else if (arg.startsWith('-psn')) {
|
||||||
// Ignore OS X launchd "process serial number" argument
|
// Ignore OS X launchd "process serial number" argument
|
||||||
// More: https://github.com/feross/webtorrent-desktop/issues/214
|
// More: https://github.com/feross/webtorrent-desktop/issues/214
|
||||||
} else {
|
} else {
|
||||||
windows.main.send('dispatch', 'onOpen', arg)
|
pathsToOpen.push(arg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (pathsToOpen.length > 0) openFilePaths(pathsToOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send files to the renderer process
|
||||||
|
// Opening files means either adding torrents, creating and seeding a torrent
|
||||||
|
// from files, or adding subtitles
|
||||||
|
function openFilePaths (paths) {
|
||||||
|
windows.main.send('dispatch', 'onOpen', paths)
|
||||||
}
|
}
|
||||||
|
|||||||
105
main/ipc.js
105
main/ipc.js
@@ -6,25 +6,29 @@ var electron = require('electron')
|
|||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
var ipcMain = electron.ipcMain
|
var ipcMain = electron.ipcMain
|
||||||
var powerSaveBlocker = electron.powerSaveBlocker
|
|
||||||
|
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
var shortcuts = require('./shortcuts')
|
var shortcuts = require('./shortcuts')
|
||||||
|
var vlc = require('./vlc')
|
||||||
|
|
||||||
// has to be a number, not a boolean, and undefined throws an error
|
// has to be a number, not a boolean, and undefined throws an error
|
||||||
var powerSaveBlockID = 0
|
var powerSaveBlockerId = 0
|
||||||
|
|
||||||
|
// messages from the main process, to be sent once the WebTorrent process starts
|
||||||
|
var messageQueueMainToWebTorrent = []
|
||||||
|
|
||||||
|
// holds a ChildProcess while we're playing a video in VLC, null otherwise
|
||||||
|
var vlcProcess
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
ipcMain.on('ipcReady', function (e) {
|
ipcMain.on('ipcReady', function (e) {
|
||||||
|
windows.main.show()
|
||||||
app.ipcReady = true
|
app.ipcReady = true
|
||||||
app.emit('ipcReady')
|
app.emit('ipcReady')
|
||||||
windows.main.show()
|
|
||||||
console.timeEnd('init')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var messageQueueMainToWebTorrent = []
|
|
||||||
ipcMain.on('ipcReadyWebTorrent', function (e) {
|
ipcMain.on('ipcReadyWebTorrent', function (e) {
|
||||||
app.ipcReadyWebTorrent = true
|
app.ipcReadyWebTorrent = true
|
||||||
log('sending %d queued messages from the main win to the webtorrent window',
|
log('sending %d queued messages from the main win to the webtorrent window',
|
||||||
@@ -36,14 +40,13 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
|
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
|
||||||
ipcMain.on('showOpenSeedFiles', menu.showOpenSeedFiles)
|
|
||||||
|
|
||||||
ipcMain.on('setBounds', function (e, bounds, maximize) {
|
ipcMain.on('setBounds', function (e, bounds, maximize) {
|
||||||
setBounds(bounds, maximize)
|
setBounds(bounds, maximize)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('setAspectRatio', function (e, aspectRatio, extraSize) {
|
ipcMain.on('setAspectRatio', function (e, aspectRatio) {
|
||||||
setAspectRatio(aspectRatio, extraSize)
|
setAspectRatio(aspectRatio)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('setBadge', function (e, text) {
|
ipcMain.on('setBadge', function (e, text) {
|
||||||
@@ -63,26 +66,83 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('openItem', function (e, path) {
|
ipcMain.on('openItem', function (e, path) {
|
||||||
log('opening file or folder: ' + path)
|
log('open item: ' + path)
|
||||||
electron.shell.openItem(path)
|
electron.shell.openItem(path)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.on('showItemInFolder', function (e, path) {
|
||||||
|
log('show item in folder: ' + path)
|
||||||
|
electron.shell.showItemInFolder(path)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.on('blockPowerSave', blockPowerSave)
|
ipcMain.on('blockPowerSave', blockPowerSave)
|
||||||
|
|
||||||
ipcMain.on('unblockPowerSave', unblockPowerSave)
|
ipcMain.on('unblockPowerSave', unblockPowerSave)
|
||||||
|
|
||||||
ipcMain.on('onPlayerOpen', function () {
|
ipcMain.on('onPlayerOpen', function () {
|
||||||
menu.onPlayerOpen()
|
menu.onPlayerOpen()
|
||||||
shortcuts.registerPlayerShortcuts()
|
shortcuts.onPlayerOpen()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('onPlayerClose', function () {
|
ipcMain.on('onPlayerClose', function () {
|
||||||
menu.onPlayerClose()
|
menu.onPlayerClose()
|
||||||
shortcuts.unregisterPlayerShortcuts()
|
shortcuts.onPlayerOpen()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('focusWindow', function (e, windowName) {
|
ipcMain.on('focusWindow', function (e, windowName) {
|
||||||
windows.focusWindow(windows[windowName])
|
windows.focusWindow(windows[windowName])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.on('downloadFinished', function (e, filePath) {
|
||||||
|
if (app.dock) {
|
||||||
|
// Bounces the Downloads stack if the filePath is inside the Downloads folder.
|
||||||
|
app.dock.downloadFinished(filePath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('checkForVLC', function (e) {
|
||||||
|
vlc.checkForVLC(function (isInstalled) {
|
||||||
|
windows.main.send('checkForVLC', isInstalled)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('vlcPlay', function (e, url) {
|
||||||
|
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url]
|
||||||
|
console.log('Running vlc ' + args.join(' '))
|
||||||
|
|
||||||
|
vlc.spawn(args, function (err, proc) {
|
||||||
|
if (err) return windows.main.send('dispatch', 'vlcNotFound')
|
||||||
|
vlcProcess = proc
|
||||||
|
|
||||||
|
// If it works, close the modal after a second
|
||||||
|
var closeModalTimeout = setTimeout(() =>
|
||||||
|
windows.main.send('dispatch', 'exitModal'), 1000)
|
||||||
|
|
||||||
|
vlcProcess.on('close', function (code) {
|
||||||
|
clearTimeout(closeModalTimeout)
|
||||||
|
if (!vlcProcess) return // Killed
|
||||||
|
console.log('VLC exited with code ', code)
|
||||||
|
if (code === 0) {
|
||||||
|
windows.main.send('dispatch', 'backToList')
|
||||||
|
} else {
|
||||||
|
windows.main.send('dispatch', 'vlcNotFound')
|
||||||
|
}
|
||||||
|
vlcProcess = null
|
||||||
|
})
|
||||||
|
|
||||||
|
vlcProcess.on('error', function (e) {
|
||||||
|
console.log('VLC error', e)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('vlcQuit', function () {
|
||||||
|
if (!vlcProcess) return
|
||||||
|
console.log('Killing VLC, pid ' + vlcProcess.pid)
|
||||||
|
vlcProcess.kill('SIGKILL') // kill -9
|
||||||
|
vlcProcess = null
|
||||||
|
})
|
||||||
|
|
||||||
// Capture all events
|
// Capture all events
|
||||||
var oldEmit = ipcMain.emit
|
var oldEmit = ipcMain.emit
|
||||||
ipcMain.emit = function (name, e, ...args) {
|
ipcMain.emit = function (name, e, ...args) {
|
||||||
@@ -142,8 +202,7 @@ function setBounds (bounds, maximize) {
|
|||||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
||||||
if (bounds.x === null && bounds.y === null) {
|
if (bounds.x === null && bounds.y === null) {
|
||||||
// X and Y not specified? By default, center on current screen
|
// X and Y not specified? By default, center on current screen
|
||||||
var screen = require('screen')
|
var scr = electron.screen.getDisplayMatching(windows.main.getBounds())
|
||||||
var scr = screen.getDisplayMatching(windows.main.getBounds())
|
|
||||||
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
||||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||||
@@ -154,17 +213,19 @@ function setBounds (bounds, maximize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAspectRatio (aspectRatio, extraSize) {
|
function setAspectRatio (aspectRatio) {
|
||||||
log('setAspectRatio %o %o', aspectRatio, extraSize)
|
log('setAspectRatio %o', aspectRatio)
|
||||||
if (windows.main) {
|
if (windows.main) {
|
||||||
windows.main.setAspectRatio(aspectRatio, extraSize)
|
windows.main.setAspectRatio(aspectRatio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display string in dock badging area (OS X)
|
// Display string in dock badging area (OS X)
|
||||||
function setBadge (text) {
|
function setBadge (text) {
|
||||||
log('setBadge %s', text)
|
log('setBadge %s', text)
|
||||||
if (app.dock) app.dock.setBadge(String(text))
|
if (app.dock) {
|
||||||
|
app.dock.setBadge(String(text))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
|
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
|
||||||
@@ -176,13 +237,13 @@ function setProgress (progress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function blockPowerSave () {
|
function blockPowerSave () {
|
||||||
powerSaveBlockID = powerSaveBlocker.start('prevent-display-sleep')
|
powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||||
log('blockPowerSave %d', powerSaveBlockID)
|
log('blockPowerSave %d', powerSaveBlockerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function unblockPowerSave () {
|
function unblockPowerSave () {
|
||||||
if (powerSaveBlocker.isStarted(powerSaveBlockID)) {
|
if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) {
|
||||||
powerSaveBlocker.stop(powerSaveBlockID)
|
electron.powerSaveBlocker.stop(powerSaveBlockerId)
|
||||||
log('unblockPowerSave %d', powerSaveBlockID)
|
log('unblockPowerSave %d', powerSaveBlockerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
348
main/menu.js
348
main/menu.js
@@ -1,11 +1,14 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
|
onPlayerClose,
|
||||||
|
onPlayerOpen,
|
||||||
onToggleFullScreen,
|
onToggleFullScreen,
|
||||||
onWindowHide,
|
onWindowHide,
|
||||||
onWindowShow,
|
onWindowShow,
|
||||||
onPlayerOpen,
|
|
||||||
onPlayerClose,
|
// TODO: move these out of menu.js -- they don't belong here
|
||||||
showOpenSeedFiles,
|
showOpenSeedFiles,
|
||||||
|
showOpenTorrentAddress,
|
||||||
showOpenTorrentFile,
|
showOpenTorrentFile,
|
||||||
toggleFullScreen
|
toggleFullScreen
|
||||||
}
|
}
|
||||||
@@ -18,20 +21,26 @@ var config = require('../config')
|
|||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
var appMenu, dockMenu
|
var appMenu
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
|
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
|
||||||
electron.Menu.setApplicationMenu(appMenu)
|
electron.Menu.setApplicationMenu(appMenu)
|
||||||
|
|
||||||
dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
|
if (app.dock) {
|
||||||
if (app.dock) app.dock.setMenu(dockMenu)
|
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
|
||||||
|
app.dock.setMenu(dockMenu)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullScreen (flag) {
|
function toggleFullScreen (flag) {
|
||||||
log('toggleFullScreen %s', flag)
|
log('toggleFullScreen %s', flag)
|
||||||
if (windows.main && windows.main.isVisible()) {
|
if (windows.main && windows.main.isVisible()) {
|
||||||
flag = flag != null ? flag : !windows.main.isFullScreen()
|
flag = flag != null ? flag : !windows.main.isFullScreen()
|
||||||
|
if (flag) {
|
||||||
|
// Allows the window to use the full screen in fullscreen mode (OS X).
|
||||||
|
windows.main.setAspectRatio(0)
|
||||||
|
}
|
||||||
windows.main.setFullScreen(flag)
|
windows.main.setFullScreen(flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,6 +55,25 @@ function toggleFloatOnTop (flag) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleDevTools () {
|
||||||
|
log('toggleDevTools')
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.toggleDevTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWebTorrentWindow () {
|
||||||
|
log('showWebTorrentWindow')
|
||||||
|
windows.webtorrent.show()
|
||||||
|
windows.webtorrent.webContents.openDevTools({ detach: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPause () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'playPause')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function increaseVolume () {
|
function increaseVolume () {
|
||||||
if (windows.main) {
|
if (windows.main) {
|
||||||
windows.main.send('dispatch', 'changeVolume', 0.1)
|
windows.main.send('dispatch', 'changeVolume', 0.1)
|
||||||
@@ -58,16 +86,47 @@ function decreaseVolume () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDevTools () {
|
function openSubtitles () {
|
||||||
log('toggleDevTools')
|
|
||||||
if (windows.main) {
|
if (windows.main) {
|
||||||
windows.main.toggleDevTools()
|
windows.main.send('dispatch', 'openSubtitles')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWebTorrentWindow () {
|
function skipForward () {
|
||||||
windows.webtorrent.show()
|
if (windows.main) {
|
||||||
windows.webtorrent.webContents.openDevTools({ detach: true })
|
windows.main.send('dispatch', 'skip', 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipBack () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'skip', -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function increasePlaybackRate () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'changePlaybackRate', 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decreasePlaybackRate () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'changePlaybackRate', -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the preferences window
|
||||||
|
function showPreferences () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'preferences')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeBack () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'escapeBack')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowShow () {
|
function onWindowShow () {
|
||||||
@@ -83,13 +142,27 @@ function onWindowHide () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerOpen () {
|
function onPlayerOpen () {
|
||||||
|
log('onPlayerOpen')
|
||||||
|
getMenuItem('Play/Pause').enabled = true
|
||||||
getMenuItem('Increase Volume').enabled = true
|
getMenuItem('Increase Volume').enabled = true
|
||||||
getMenuItem('Decrease Volume').enabled = true
|
getMenuItem('Decrease Volume').enabled = true
|
||||||
|
getMenuItem('Add Subtitles File...').enabled = true
|
||||||
|
getMenuItem('Step Forward').enabled = true
|
||||||
|
getMenuItem('Step Backward').enabled = true
|
||||||
|
getMenuItem('Increase Speed').enabled = true
|
||||||
|
getMenuItem('Decrease Speed').enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function onPlayerClose () {
|
||||||
|
log('onPlayerClose')
|
||||||
|
getMenuItem('Play/Pause').enabled = false
|
||||||
getMenuItem('Increase Volume').enabled = false
|
getMenuItem('Increase Volume').enabled = false
|
||||||
getMenuItem('Decrease Volume').enabled = false
|
getMenuItem('Decrease Volume').enabled = false
|
||||||
|
getMenuItem('Add Subtitles File...').enabled = false
|
||||||
|
getMenuItem('Step Forward').enabled = false
|
||||||
|
getMenuItem('Step Backward').enabled = false
|
||||||
|
getMenuItem('Increase Speed').enabled = false
|
||||||
|
getMenuItem('Decrease Speed').enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleFullScreen (isFullScreen) {
|
function onToggleFullScreen (isFullScreen) {
|
||||||
@@ -108,17 +181,27 @@ function getMenuItem (label) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompts the user for a file or folder, then makes a torrent out of the data
|
// 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
|
||||||
|
windows.main.send('dispatch', 'showCreateTorrent', selectedPaths)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompts the user for a file or directory, then creates a torrent. Only allows a single
|
||||||
|
// selection. To create a multi-file torrent, the user must select a directory.
|
||||||
function showOpenSeedFiles () {
|
function showOpenSeedFiles () {
|
||||||
// Allow only a single selection
|
|
||||||
// To create a multi-file torrent, the user must select a folder
|
|
||||||
electron.dialog.showOpenDialog({
|
electron.dialog.showOpenDialog({
|
||||||
title: 'Select a file or folder for the torrent file.',
|
title: 'Select a file or folder for the torrent file.',
|
||||||
properties: [ 'openFile', 'openDirectory' ]
|
properties: [ 'openFile', 'openDirectory' ]
|
||||||
}, function (filenames) {
|
}, function (selectedPaths) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(selectedPaths)) return
|
||||||
var fileOrFolder = filenames[0]
|
windows.main.send('dispatch', 'showCreateTorrent', selectedPaths)
|
||||||
windows.main.send('dispatch', 'showCreateTorrent', fileOrFolder)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,10 +211,10 @@ function showOpenTorrentFile () {
|
|||||||
title: 'Select a .torrent file to open.',
|
title: 'Select a .torrent file to open.',
|
||||||
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
||||||
properties: [ 'openFile', 'multiSelections' ]
|
properties: [ 'openFile', 'multiSelections' ]
|
||||||
}, function (filenames) {
|
}, function (selectedPaths) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(selectedPaths)) return
|
||||||
filenames.forEach(function (filename) {
|
selectedPaths.forEach(function (selectedPath) {
|
||||||
windows.main.send('dispatch', 'addTorrent', filename)
|
windows.main.send('dispatch', 'addTorrent', selectedPath)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -142,44 +225,38 @@ function showOpenTorrentAddress () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAppMenuTemplate () {
|
function getAppMenuTemplate () {
|
||||||
var fileMenu = [
|
|
||||||
{
|
|
||||||
label: 'Create New Torrent...',
|
|
||||||
accelerator: 'CmdOrCtrl+N',
|
|
||||||
click: showOpenSeedFiles
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open Torrent File...',
|
|
||||||
accelerator: 'CmdOrCtrl+O',
|
|
||||||
click: showOpenTorrentFile
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open Torrent Address...',
|
|
||||||
accelerator: 'CmdOrCtrl+U',
|
|
||||||
click: showOpenTorrentAddress
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: process.platform === 'windows' ? 'Close' : 'Close Window',
|
|
||||||
accelerator: 'CmdOrCtrl+W',
|
|
||||||
role: 'close'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// File > Quit for Linux users with distros where the system tray is broken
|
|
||||||
if (process.platform === 'linux') {
|
|
||||||
fileMenu.push({
|
|
||||||
label: 'Quit',
|
|
||||||
click: () => app.quit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var template = [
|
var template = [
|
||||||
{
|
{
|
||||||
label: 'File',
|
label: 'File',
|
||||||
submenu: fileMenu
|
submenu: [
|
||||||
|
{
|
||||||
|
label: process.platform === 'darwin'
|
||||||
|
? 'Create New Torrent...'
|
||||||
|
: 'Create New Torrent from Folder...',
|
||||||
|
accelerator: 'CmdOrCtrl+N',
|
||||||
|
click: showOpenSeedFiles
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Open Torrent File...',
|
||||||
|
accelerator: 'CmdOrCtrl+O',
|
||||||
|
click: showOpenTorrentFile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Open Torrent Address...',
|
||||||
|
accelerator: 'CmdOrCtrl+U',
|
||||||
|
click: showOpenTorrentAddress
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: process.platform === 'win32'
|
||||||
|
? 'Close'
|
||||||
|
: 'Close Window',
|
||||||
|
accelerator: 'CmdOrCtrl+W',
|
||||||
|
role: 'close'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
@@ -203,6 +280,14 @@ function getAppMenuTemplate () {
|
|||||||
label: 'Select All',
|
label: 'Select All',
|
||||||
accelerator: 'CmdOrCtrl+A',
|
accelerator: 'CmdOrCtrl+A',
|
||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
accelerator: 'CmdOrCtrl+,',
|
||||||
|
click: () => showPreferences()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -225,21 +310,6 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Increase Volume',
|
|
||||||
accelerator: 'CmdOrCtrl+Up',
|
|
||||||
click: increaseVolume,
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Decrease Volume',
|
|
||||||
accelerator: 'CmdOrCtrl+Down',
|
|
||||||
click: decreaseVolume,
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Developer',
|
label: 'Developer',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -258,17 +328,78 @@ function getAppMenuTemplate () {
|
|||||||
click: showWebTorrentWindow
|
click: showWebTorrentWindow
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Go Back',
|
||||||
|
accelerator: 'Esc',
|
||||||
|
click: escapeBack
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Window',
|
label: 'Playback',
|
||||||
role: 'window',
|
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Minimize',
|
label: 'Play/Pause',
|
||||||
accelerator: 'CmdOrCtrl+M',
|
accelerator: 'Space',
|
||||||
role: 'minimize'
|
click: playPause,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Increase Volume',
|
||||||
|
accelerator: 'CmdOrCtrl+Up',
|
||||||
|
click: increaseVolume,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Decrease Volume',
|
||||||
|
accelerator: 'CmdOrCtrl+Down',
|
||||||
|
click: decreaseVolume,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Step Forward',
|
||||||
|
accelerator: 'CmdOrCtrl+Alt+Right',
|
||||||
|
click: skipForward,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Step Backward',
|
||||||
|
accelerator: 'CmdOrCtrl+Alt+Left',
|
||||||
|
click: skipBack,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Increase Speed',
|
||||||
|
accelerator: 'CmdOrCtrl+=',
|
||||||
|
click: increasePlaybackRate,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Decrease Speed',
|
||||||
|
accelerator: 'CmdOrCtrl+-',
|
||||||
|
click: decreasePlaybackRate,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add Subtitles File...',
|
||||||
|
click: openSubtitles,
|
||||||
|
enabled: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -278,7 +409,7 @@ function getAppMenuTemplate () {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Learn more about ' + config.APP_NAME,
|
label: 'Learn more about ' + config.APP_NAME,
|
||||||
click: () => electron.shell.openExternal('https://webtorrent.io')
|
click: () => electron.shell.openExternal(config.HOME_PAGE_URL)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Contribute on GitHub',
|
label: 'Contribute on GitHub',
|
||||||
@@ -289,14 +420,14 @@ function getAppMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Report an Issue...',
|
label: 'Report an Issue...',
|
||||||
click: () => electron.shell.openExternal(config.GITHUB_URL + '/issues')
|
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
// WebTorrent menu (OS X)
|
// Add WebTorrent app menu (OS X)
|
||||||
template.unshift({
|
template.unshift({
|
||||||
label: config.APP_NAME,
|
label: config.APP_NAME,
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -307,6 +438,14 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
accelerator: 'Cmd+,',
|
||||||
|
click: () => showPreferences()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Services',
|
label: 'Services',
|
||||||
role: 'services',
|
role: 'services',
|
||||||
@@ -340,17 +479,35 @@ function getAppMenuTemplate () {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
// Window menu (OS X)
|
// Add Window menu (OS X)
|
||||||
template[4].submenu.push(
|
template.splice(5, 0, {
|
||||||
{
|
label: 'Window',
|
||||||
type: 'separator'
|
role: 'window',
|
||||||
},
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Bring All to Front',
|
label: 'Minimize',
|
||||||
role: 'front'
|
accelerator: 'CmdOrCtrl+M',
|
||||||
}
|
role: 'minimize'
|
||||||
)
|
},
|
||||||
} else {
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bring All to Front',
|
||||||
|
role: 'front'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// In Linux and Windows it is not possible to open both folders and files
|
||||||
|
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||||
|
// File menu (Windows, Linux)
|
||||||
|
template[0].submenu.unshift({
|
||||||
|
label: 'Create New Torrent from File...',
|
||||||
|
click: showOpenSeedFile
|
||||||
|
})
|
||||||
|
|
||||||
// Help menu (Windows, Linux)
|
// Help menu (Windows, Linux)
|
||||||
template[4].submenu.push(
|
template[4].submenu.push(
|
||||||
{
|
{
|
||||||
@@ -362,6 +519,15 @@ function getAppMenuTemplate () {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Add "File > Quit" menu item so Linux distros where the system tray icon is missing
|
||||||
|
// will have a way to quit the app.
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
// File menu (Linux)
|
||||||
|
template[0].submenu.push({
|
||||||
|
label: 'Quit',
|
||||||
|
click: () => app.quit()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
onPlayerClose,
|
||||||
registerPlayerShortcuts,
|
onPlayerOpen
|
||||||
unregisterPlayerShortcuts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
var localShortcut = require('electron-localshortcut')
|
|
||||||
|
|
||||||
var globalShortcut = electron.globalShortcut
|
|
||||||
|
|
||||||
var menu = require('./menu')
|
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
function init () {
|
function onPlayerOpen () {
|
||||||
// ⌘+Shift+F is an alternative fullscreen shortcut to the ones defined in menu.js.
|
// Register special "media key" for play/pause, available on some keyboards
|
||||||
// Electron does not support multiple accelerators for a single menu item, so this
|
electron.globalShortcut.register(
|
||||||
// is registered separately here.
|
'MediaPlayPause',
|
||||||
localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen)
|
() => windows.main.send('dispatch', 'playPause')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerPlayerShortcuts () {
|
function onPlayerClose () {
|
||||||
// Special "media key" for play/pause, available on some keyboards
|
electron.globalShortcut.unregister('MediaPlayPause')
|
||||||
globalShortcut.register('MediaPlayPause', () => windows.main.send('dispatch', 'playPause'))
|
|
||||||
}
|
|
||||||
|
|
||||||
function unregisterPlayerShortcuts () {
|
|
||||||
globalShortcut.unregister('MediaPlayPause')
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ var path = require('path')
|
|||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
var Menu = electron.Menu
|
|
||||||
var Tray = electron.Tray
|
|
||||||
|
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
@@ -35,7 +33,7 @@ function hasTray () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createTrayIcon () {
|
function createTrayIcon () {
|
||||||
trayIcon = new Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
|
trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
|
||||||
|
|
||||||
// On Windows, left click to open the app, right click for context menu
|
// On Windows, left click to open the app, right click for context menu
|
||||||
// On Linux, any click (right or left) opens the context menu
|
// On Linux, any click (right or left) opens the context menu
|
||||||
@@ -66,7 +64,7 @@ function updateTrayMenu () {
|
|||||||
} else {
|
} else {
|
||||||
showHideMenuItem = { label: 'Show', click: showApp }
|
showHideMenuItem = { label: 'Show', click: showApp }
|
||||||
}
|
}
|
||||||
var contextMenu = Menu.buildFromTemplate([
|
var contextMenu = electron.Menu.buildFromTemplate([
|
||||||
showHideMenuItem,
|
showHideMenuItem,
|
||||||
{ label: 'Quit', click: () => app.quit() }
|
{ label: 'Quit', click: () => app.quit() }
|
||||||
])
|
])
|
||||||
|
|||||||
76
main/updater.js
Normal file
76
main/updater.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
module.exports = {
|
||||||
|
init
|
||||||
|
}
|
||||||
|
|
||||||
|
var electron = require('electron')
|
||||||
|
var get = require('simple-get')
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
var log = require('./log')
|
||||||
|
var windows = require('./windows')
|
||||||
|
|
||||||
|
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||||
|
'?version=' + config.APP_VERSION +
|
||||||
|
'&platform=' + process.platform
|
||||||
|
|
||||||
|
function init () {
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
initLinux()
|
||||||
|
} else {
|
||||||
|
initDarwinWin32()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Electron auto-updater does not support Linux yet, so manually check for updates and
|
||||||
|
// `show the user a modal notification.
|
||||||
|
function initLinux () {
|
||||||
|
get.concat(AUTO_UPDATE_URL, onResponse)
|
||||||
|
|
||||||
|
function onResponse (err, res, data) {
|
||||||
|
if (err) return log(`Update error: ${err.message}`)
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
// Update available
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data)
|
||||||
|
} catch (err) {
|
||||||
|
return log(`Update error: Invalid JSON response: ${err.message}`)
|
||||||
|
}
|
||||||
|
windows.main.send('dispatch', 'updateAvailable', data.version)
|
||||||
|
} else if (res.statusCode === 204) {
|
||||||
|
// No update available
|
||||||
|
} else {
|
||||||
|
// Unexpected status code
|
||||||
|
log(`Update error: Unexpected status code: ${res.statusCode}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDarwinWin32 () {
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'error',
|
||||||
|
(err) => log.error(`Update error: ${err.message}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'checking-for-update',
|
||||||
|
() => log('Checking for update')
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'update-available',
|
||||||
|
() => log('Update available')
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'update-not-available',
|
||||||
|
() => log('Update not available')
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.on(
|
||||||
|
'update-downloaded',
|
||||||
|
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
|
||||||
|
electron.autoUpdater.checkForUpdates()
|
||||||
|
}
|
||||||
22
main/vlc.js
Normal file
22
main/vlc.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
checkForVLC,
|
||||||
|
spawn
|
||||||
|
}
|
||||||
|
|
||||||
|
var cp = require('child_process')
|
||||||
|
var vlcCommand = require('vlc-command')
|
||||||
|
|
||||||
|
// Finds if VLC is installed on Mac, Windows, or Linux.
|
||||||
|
// Calls back with true or false: whether VLC was detected
|
||||||
|
function checkForVLC (cb) {
|
||||||
|
vlcCommand((err) => cb(!err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawns VLC with child_process.spawn() to return a ChildProcess object
|
||||||
|
// Calls back with (err, childProcess)
|
||||||
|
function spawn (args, cb) {
|
||||||
|
vlcCommand(function (err, vlcPath) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
cb(null, cp.spawn(vlcPath, args))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ var windows = module.exports = {
|
|||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
|
var app = electron.app
|
||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var tray = require('./tray')
|
var tray = require('./tray')
|
||||||
@@ -68,7 +70,7 @@ function createWebTorrentHiddenWindow () {
|
|||||||
|
|
||||||
// Prevent killing the WebTorrent process
|
// Prevent killing the WebTorrent process
|
||||||
win.on('close', function (e) {
|
win.on('close', function (e) {
|
||||||
if (!electron.app.isQuitting) {
|
if (!app.isQuitting) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
win.hide()
|
win.hide()
|
||||||
}
|
}
|
||||||
@@ -79,6 +81,9 @@ function createWebTorrentHiddenWindow () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var HEADER_HEIGHT = 37
|
||||||
|
var TORRENT_HEIGHT = 100
|
||||||
|
|
||||||
function createMainWindow () {
|
function createMainWindow () {
|
||||||
if (windows.main) {
|
if (windows.main) {
|
||||||
return focusWindow(windows.main)
|
return focusWindow(windows.main)
|
||||||
@@ -89,14 +94,17 @@ function createMainWindow () {
|
|||||||
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
|
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
|
||||||
minWidth: config.WINDOW_MIN_WIDTH,
|
minWidth: config.WINDOW_MIN_WIDTH,
|
||||||
minHeight: config.WINDOW_MIN_HEIGHT,
|
minHeight: config.WINDOW_MIN_HEIGHT,
|
||||||
show: false, // Hide window until DOM finishes loading
|
show: false, // Hide window until renderer sends 'ipcReady' event
|
||||||
title: config.APP_WINDOW_TITLE,
|
title: config.APP_WINDOW_TITLE,
|
||||||
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
||||||
useContentSize: true, // Specify web page size without OS chrome
|
useContentSize: true, // Specify web page size without OS chrome
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 38 + (120 * 5) // header height + 4 torrents
|
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
||||||
})
|
})
|
||||||
win.loadURL(config.WINDOW_MAIN)
|
win.loadURL(config.WINDOW_MAIN)
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
win.setSheetOffset(HEADER_HEIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
win.webContents.on('dom-ready', function () {
|
win.webContents.on('dom-ready', function () {
|
||||||
menu.onToggleFullScreen()
|
menu.onToggleFullScreen()
|
||||||
@@ -110,8 +118,8 @@ function createMainWindow () {
|
|||||||
|
|
||||||
win.on('close', function (e) {
|
win.on('close', function (e) {
|
||||||
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
||||||
electron.app.quit()
|
app.quit()
|
||||||
} else if (!electron.app.isQuitting) {
|
} else if (!app.isQuitting) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
win.hide()
|
win.hide()
|
||||||
win.send('dispatch', 'backToList')
|
win.send('dispatch', 'backToList')
|
||||||
|
|||||||
42
package.json
42
package.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
||||||
"version": "0.3.3",
|
"version": "0.6.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Feross Aboukhadijeh",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@feross.org",
|
"email": "feross@feross.org",
|
||||||
"url": "http://feross.org"
|
"url": "https://webtorrent.io"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"webtorrent-desktop": "./bin/cmd.js"
|
"webtorrent-desktop": "./bin/cmd.js"
|
||||||
@@ -15,53 +15,62 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"airplay-js": "guerrerocarlos/node-airplay-js",
|
"airplay-js": "guerrerocarlos/node-airplay-js",
|
||||||
"application-config": "feross/node-application-config",
|
"application-config": "^0.2.1",
|
||||||
"bitfield": "^1.0.2",
|
"bitfield": "^1.0.2",
|
||||||
"chromecasts": "^1.8.0",
|
"chromecasts": "^1.8.0",
|
||||||
"concat-stream": "^1.5.1",
|
|
||||||
"create-torrent": "^3.24.5",
|
"create-torrent": "^3.24.5",
|
||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.0.3",
|
"dlnacasts": "^0.1.0",
|
||||||
"drag-drop": "^2.11.0",
|
"drag-drop": "^2.11.0",
|
||||||
"electron-localshortcut": "^0.6.0",
|
"electron-prebuilt": "1.1.1",
|
||||||
"electron-prebuilt": "0.37.6",
|
|
||||||
"fs-extra": "^0.27.0",
|
"fs-extra": "^0.27.0",
|
||||||
"hyperx": "^2.0.2",
|
"hyperx": "^2.0.2",
|
||||||
|
"iso-639-1": "^1.2.1",
|
||||||
"languagedetect": "^1.1.1",
|
"languagedetect": "^1.1.1",
|
||||||
"main-loop": "^3.2.0",
|
"main-loop": "^3.2.0",
|
||||||
"musicmetadata": "^2.0.2",
|
"musicmetadata": "^2.0.2",
|
||||||
"network-address": "^1.1.0",
|
"network-address": "^1.1.0",
|
||||||
"prettier-bytes": "^1.0.1",
|
"prettier-bytes": "^1.0.1",
|
||||||
|
"run-parallel": "^1.1.6",
|
||||||
|
"simple-concat": "^1.0.0",
|
||||||
"simple-get": "^2.0.0",
|
"simple-get": "^2.0.0",
|
||||||
"srt-to-vtt": "^1.1.1",
|
"srt-to-vtt": "^1.1.1",
|
||||||
"upload-element": "^1.0.1",
|
|
||||||
"virtual-dom": "^2.1.1",
|
"virtual-dom": "^2.1.1",
|
||||||
|
"vlc-command": "^1.0.1",
|
||||||
"webtorrent": "0.x",
|
"webtorrent": "0.x",
|
||||||
"winreg": "^1.1.1"
|
"winreg": "^1.2.0",
|
||||||
|
"zero-fill": "^2.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-zip": "^1.0.0",
|
"cross-zip": "^2.0.1",
|
||||||
"electron-osx-sign": "^0.3.0",
|
"electron-osx-sign": "^0.3.0",
|
||||||
"electron-packager": "^7.0.0",
|
"electron-packager": "^7.0.0",
|
||||||
"electron-winstaller": "feross/windows-installer#build",
|
"electron-winstaller": "^2.3.0",
|
||||||
"gh-release": "^2.0.3",
|
"gh-release": "^2.0.3",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"nobin-debian-installer": "^0.0.9",
|
"mkdirp": "^0.5.1",
|
||||||
|
"nobin-debian-installer": "^0.0.10",
|
||||||
|
"open": "0.0.5",
|
||||||
"plist": "^1.2.0",
|
"plist": "^1.2.0",
|
||||||
|
"rimraf": "^2.5.2",
|
||||||
"run-series": "^1.1.4",
|
"run-series": "^1.1.4",
|
||||||
"standard": "^6.0.5"
|
"standard": "^7.0.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://webtorrent.io",
|
"homepage": "https://webtorrent.io",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"desktop",
|
"desktop",
|
||||||
"electron",
|
"electron",
|
||||||
"electron-app",
|
"electron-app",
|
||||||
|
"hybrid webtorrent client",
|
||||||
|
"mad science",
|
||||||
|
"torrent client",
|
||||||
|
"torrent",
|
||||||
"webtorrent"
|
"webtorrent"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"appdmg": "^0.3.6"
|
"appdmg": "^0.4.3"
|
||||||
},
|
},
|
||||||
"productName": "WebTorrent",
|
"productName": "WebTorrent",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -70,9 +79,10 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "node ./bin/clean.js",
|
"clean": "node ./bin/clean.js",
|
||||||
|
"open-config": "node ./bin/open-config.js",
|
||||||
"package": "node ./bin/package.js",
|
"package": "node ./bin/package.js",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"test": "standard",
|
"test": "standard && node ./bin/check-deps.js",
|
||||||
"update-authors": "./bin/update-authors.sh"
|
"update-authors": "./bin/update-authors.sh"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<body>
|
<body>
|
||||||
<img src="../static/WebTorrent.png">
|
<img src="../static/WebTorrent.png">
|
||||||
<h1>WebTorrent</h1>
|
<h1>WebTorrent</h1>
|
||||||
<p>Version <script>document.write(require('../package.json').version)</script></p>
|
<p>
|
||||||
|
Version <script>document.write(require('../package.json').version)</script>
|
||||||
|
(<script>document.write(require('webtorrent/package.json').version)</script>)
|
||||||
|
</p>
|
||||||
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -50,19 +50,22 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
animation: fadein 0.3s;
|
|
||||||
background: rgb(40, 40, 40);
|
background: rgb(40, 40, 40);
|
||||||
|
animation: fadein 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app:not(.is-focused) {
|
.app:not(.is-focused) {
|
||||||
@@ -94,11 +97,20 @@ table {
|
|||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:not(.disabled):hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* UTILITY CLASSES
|
* UTILITY CLASSES
|
||||||
*/
|
*/
|
||||||
@@ -109,18 +121,14 @@ table {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.float-left {
|
||||||
opacity: 0.3;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-collapse {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-collapse.expanded::before {
|
.expand-collapse.expanded::before {
|
||||||
content: '▲'
|
content: '▲'
|
||||||
}
|
}
|
||||||
@@ -148,8 +156,8 @@ table {
|
|||||||
.header {
|
.header {
|
||||||
background: rgb(40, 40, 40);
|
background: rgb(40, 40, 40);
|
||||||
border-bottom: 1px solid rgb(20, 20, 20);
|
border-bottom: 1px solid rgb(20, 20, 20);
|
||||||
height: 37px; /* vertically center OS menu buttons (OS X) */
|
height: 38px; /* vertically center OS menu buttons (OS X) */
|
||||||
padding-top: 6px;
|
padding-top: 7px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -168,7 +176,13 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app.view-player .header {
|
.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 {
|
.app.hide-video-controls.view-player .header {
|
||||||
@@ -176,12 +190,8 @@ table {
|
|||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.hide-header .header {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .title {
|
.header .title {
|
||||||
opacity: 0.6;
|
opacity: 0.7;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
padding: 0 150px 0 150px;
|
padding: 0 150px 0 150px;
|
||||||
@@ -192,35 +202,22 @@ table {
|
|||||||
|
|
||||||
.header .nav {
|
.header .nav {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 9px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .nav.left {
|
.header .nav.left {
|
||||||
float: left;
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .nav.right {
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.is-darwin:not(.is-fullscreen) .header .nav.left {
|
.app.is-darwin:not(.is-fullscreen) .header .nav.left {
|
||||||
margin-left: 78px;
|
margin-left: 78px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .nav.right {
|
.header .back,
|
||||||
float: right;
|
.header .forward {
|
||||||
}
|
|
||||||
|
|
||||||
.header .nav * {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .nav .disabled {
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .nav *:not(.disabled):hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .nav .back,
|
|
||||||
.header .nav .forward {
|
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
}
|
}
|
||||||
@@ -276,7 +273,6 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal label {
|
.modal label {
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +363,6 @@ button { /* Rectangular text buttons */
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@@ -434,7 +429,7 @@ input {
|
|||||||
|
|
||||||
.torrent,
|
.torrent,
|
||||||
.torrent-placeholder {
|
.torrent-placeholder {
|
||||||
height: 120px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent:not(:last-child) {
|
.torrent:not(:last-child) {
|
||||||
@@ -447,9 +442,9 @@ input {
|
|||||||
|
|
||||||
.torrent .metadata {
|
.torrent .metadata {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 25px;
|
||||||
left: 20px;
|
left: 15px;
|
||||||
right: 20px;
|
right: 15px;
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
|
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
|
||||||
}
|
}
|
||||||
@@ -459,12 +454,15 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent .metadata span:not(:last-child)::after {
|
.torrent .metadata span:not(:last-child)::after {
|
||||||
content: ' — ';
|
content: ' • ';
|
||||||
|
opacity: 0.7;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .buttons {
|
.torrent .buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 25px;
|
top: 29px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -538,6 +536,11 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent .buttons .play.resume-position {
|
||||||
|
position: relative;
|
||||||
|
-webkit-clip-path: circle(18px at center);
|
||||||
|
}
|
||||||
|
|
||||||
.torrent .buttons .delete {
|
.torrent .buttons .delete {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
@@ -546,15 +549,13 @@ input {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .name {
|
.torrent .buttons .radial-progress {
|
||||||
font-size: 1.5em;
|
position: absolute;
|
||||||
font-weight: bold;
|
|
||||||
line-height: 1.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent .status,
|
.torrent .name {
|
||||||
.torrent .status2 {
|
font-size: 18px;
|
||||||
font-size: 1em;
|
font-weight: bold;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,11 +602,7 @@ body.drag .app::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details {
|
.torrent-details {
|
||||||
padding: 8em 20px 20px 20px;
|
padding: 8em 0 20px 0;
|
||||||
}
|
|
||||||
|
|
||||||
.torrent-details .open-folder {
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details table {
|
.torrent-details table {
|
||||||
@@ -619,27 +616,37 @@ body.drag .app::after {
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details tr:hover,
|
.torrent-details td {
|
||||||
.torrent-details .open-folder:hover {
|
vertical-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-details tr:hover {
|
||||||
background-color: rgba(200, 200, 200, 0.3);
|
background-color: rgba(200, 200, 200, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details td {
|
.torrent-details td {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
vertical-align: bottom;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details td.col-icon {
|
.torrent-details td .icon {
|
||||||
width: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent-details td.col-icon .icon {
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent-details td.col-icon {
|
||||||
|
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 {
|
.torrent-details td.col-name {
|
||||||
width: auto;
|
width: auto;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -655,6 +662,12 @@ body.drag .app::after {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent-details td.col-select {
|
||||||
|
width: 3em;
|
||||||
|
padding-right: 13px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* PLAYER
|
* PLAYER
|
||||||
*/
|
*/
|
||||||
@@ -683,7 +696,7 @@ body.drag .app::after {
|
|||||||
* PLAYER CONTROLS
|
* PLAYER CONTROLS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.player-controls {
|
.player .controls {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: rgba(40, 40, 40, 0.8);
|
background: rgba(40, 40, 40, 0.8);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -692,7 +705,63 @@ body.drag .app::after {
|
|||||||
transition: opacity 0.15s ease-out;
|
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;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fix for overflowing captions icon
|
||||||
|
* https://github.com/feross/webtorrent-desktop/issues/467
|
||||||
|
*/
|
||||||
|
max-width: 23px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .icon:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .play-pause {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .volume-slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
width: 60px;
|
||||||
|
height: 3px;
|
||||||
|
margin: 18px 8px 8px 0;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0.85;
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .time,
|
||||||
|
.player .controls .rate {
|
||||||
|
font-weight: 100;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 9px 8px 8px 8px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .icon.closed-captions {
|
||||||
|
font-size: 26px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .icon.fullscreen {
|
||||||
|
font-size: 26px;
|
||||||
|
margin-right: 15px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app.hide-video-controls .player .controls {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,13 +769,16 @@ body.drag .app::after {
|
|||||||
cursor: none;
|
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;
|
opacity: 1;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* invisible click target for scrubbing */
|
/* invisible click target for scrubbing */
|
||||||
.player-controls .scrub-bar {
|
.player .controls .scrub-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 23px; /* 3px .loading-bar plus 10px above and below */
|
height: 23px; /* 3px .loading-bar plus 10px above and below */
|
||||||
@@ -715,7 +787,7 @@ body.drag .app::after {
|
|||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .loading-bar {
|
.player .controls .loading-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: -3px;
|
top: -3px;
|
||||||
@@ -725,14 +797,14 @@ body.drag .app::after {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .loading-bar-part {
|
.player .controls .loading-bar-part {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #dd0000;
|
background-color: #dd0000;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .playback-cursor {
|
.player .controls .playback-cursor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -3px;
|
top: -3px;
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
@@ -741,86 +813,26 @@ body.drag .app::after {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
transition-property: width, height, border-radius, margin-top, margin-left;
|
transition-property: width, height, top, margin-left;
|
||||||
transition-duration: 0.1s;
|
transition-duration: 0.1s;
|
||||||
transition-timing-function: ease-out;
|
transition-timing-function: ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .play-pause {
|
.player .controls .closed-captions.active,
|
||||||
display: block;
|
.player .controls .device.active {
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 5px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .device,
|
|
||||||
.player-controls .fullscreen,
|
|
||||||
.player-controls .closed-captions,
|
|
||||||
.player-controls .volume-icon,
|
|
||||||
.player-controls .back {
|
|
||||||
display: block;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume,
|
|
||||||
.player-controls .back {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .device,
|
|
||||||
.player-controls .closed-captions,
|
|
||||||
.player-controls .fullscreen {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .fullscreen {
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume-icon,
|
|
||||||
.player-controls .device {
|
|
||||||
font-size: 18px; /* make the cast icons less huge */
|
|
||||||
margin-top: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .closed-captions.active,
|
|
||||||
.player-controls .device.active {
|
|
||||||
color: #9af;
|
color: #9af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .volume {
|
.player .controls .volume-slider::-webkit-slider-thumb {
|
||||||
display: block;
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume-icon {
|
|
||||||
float: left;
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume-slider {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 50px;
|
|
||||||
height: 3px;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
vertical-align: sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume-slider::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
opacity: 1.0;
|
width: 13px;
|
||||||
width: 10px;
|
height: 13px;
|
||||||
height: 10px;
|
|
||||||
border: 1px solid #303233;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .volume-slider:focus {
|
.player .controls .volume-slider:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,19 +842,27 @@ body.drag .app::after {
|
|||||||
|
|
||||||
.player .playback-bar:hover .playback-cursor {
|
.player .playback-bar:hover .playback-cursor {
|
||||||
top: -8px;
|
top: -8px;
|
||||||
|
margin-left: -5px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cue text position so it appears above the player controls.
|
||||||
|
*/
|
||||||
|
video::-webkit-media-text-track-container {
|
||||||
|
bottom: 60px;
|
||||||
|
transition: bottom 0.1s ease-out;
|
||||||
|
}
|
||||||
|
.app.hide-video-controls video::-webkit-media-text-track-container {
|
||||||
|
bottom: 30px;
|
||||||
|
}
|
||||||
::cue {
|
::cue {
|
||||||
background: none;
|
background: none;
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
font: 24px;
|
|
||||||
line-height: 1.3em;
|
|
||||||
text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0;
|
text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CHROMECAST / AIRPLAY CONTROLS
|
* CHROMECAST / AIRPLAY CONTROLS
|
||||||
*/
|
*/
|
||||||
@@ -892,6 +912,173 @@ body.drag .app::after {
|
|||||||
margin-right: 4px !important;
|
margin-right: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Preferences page, based on Atom settings style
|
||||||
|
*/
|
||||||
|
|
||||||
|
.preferences {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: calc(10/7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .text {
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .icon {
|
||||||
|
color: rgba(170, 170, 170, 0.6);
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .btn {
|
||||||
|
display: inline-block;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-color: #cccccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #9da5b4;
|
||||||
|
text-shadow: none;
|
||||||
|
border: 1px solid #181a1f;
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
white-space: initial;
|
||||||
|
font-size: 0.889em;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .btn .icon {
|
||||||
|
margin: 0;
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .help .icon {
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.preferences .preferences-panel .control-group + .control-group {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid #181a1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section:first {
|
||||||
|
border-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section:first-child,
|
||||||
|
.preferences .section:last-child {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section.section:empty {
|
||||||
|
padding: 0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences section .section-heading,
|
||||||
|
.preferences .section .section-heading {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #dcdcdc;
|
||||||
|
font-size: 1.75em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .sub-section-heading.icon:before,
|
||||||
|
.preferences .section-heading.icon:before {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section-heading-count {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section-body {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .sub-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .sub-section .sub-section-heading {
|
||||||
|
color: #dcdcdc;
|
||||||
|
font-size: 1.4em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preferences-panel label {
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preferences-panel .control-group + .control-group {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preferences-panel .control-group .editor-container {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preference-title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preference-description {
|
||||||
|
color: rgba(170, 170, 170, 0.6);
|
||||||
|
-webkit-user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences input {
|
||||||
|
font-size: 1.1em;
|
||||||
|
line-height: 1.15em;
|
||||||
|
max-height: none;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #a8a8a8;
|
||||||
|
border: 1px solid #181a1f;
|
||||||
|
background-color: #1b1d23;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences input::-webkit-input-placeholder {
|
||||||
|
color: rgba(170, 170, 170, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .control-group input {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .control-group input.file-picker-text {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .control-group .checkbox .icon {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MEDIA OVERLAY / AUDIO DETAILS
|
* MEDIA OVERLAY / AUDIO DETAILS
|
||||||
*/
|
*/
|
||||||
@@ -960,10 +1147,6 @@ body.drag .app::after {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.hide-header .error-popover {
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-popover.hidden {
|
.error-popover.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -987,3 +1170,70 @@ body.drag .app::after {
|
|||||||
.error-popover .error:last-child {
|
.error-popover .error:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #c44;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* RADIAL PROGRESS BAR
|
||||||
|
*/
|
||||||
|
|
||||||
|
.radial-progress {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress .circle .mask,
|
||||||
|
.radial-progress .circle .fill {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress .circle .mask {
|
||||||
|
clip: rect(0px, 16px, 16px, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress .circle .fill {
|
||||||
|
clip: rect(0px, 8px, 16px, 0px);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress .inset {
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin: 2px 0 0 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress-large {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress-large .circle .mask,
|
||||||
|
.radial-progress-large .circle .fill {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.radial-progress-large .circle .mask {
|
||||||
|
clip: rect(0px, 40px, 40px, 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress-large .circle .fill {
|
||||||
|
clip: rect(0px, 20px, 40px, 0px);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress-large .inset {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,8 @@ module.exports = {
|
|||||||
play,
|
play,
|
||||||
pause,
|
pause,
|
||||||
seek,
|
seek,
|
||||||
setVolume
|
setVolume,
|
||||||
|
setRate
|
||||||
}
|
}
|
||||||
|
|
||||||
var airplay = require('airplay-js')
|
var airplay = require('airplay-js')
|
||||||
@@ -344,6 +345,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) {
|
function seek (time) {
|
||||||
var device = getDevice()
|
var device = getDevice()
|
||||||
if (device) {
|
if (device) {
|
||||||
|
|||||||
@@ -4,81 +4,123 @@ function LocationHistory () {
|
|||||||
if (!new.target) return new LocationHistory()
|
if (!new.target) return new LocationHistory()
|
||||||
this._history = []
|
this._history = []
|
||||||
this._forward = []
|
this._forward = []
|
||||||
this._pending = null
|
this._pending = false
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.go = function (page, cb) {
|
LocationHistory.prototype.url = function () {
|
||||||
console.log('go', page)
|
return this.current() && this.current().url
|
||||||
this.clearForward()
|
|
||||||
this._go(page, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype._go = function (page, cb) {
|
|
||||||
if (this._pending) return
|
|
||||||
if (page.onbeforeload) {
|
|
||||||
this._pending = page
|
|
||||||
page.onbeforeload((err) => {
|
|
||||||
if (this._pending !== page) return /* navigation was cancelled */
|
|
||||||
this._pending = null
|
|
||||||
if (err) {
|
|
||||||
if (cb) cb(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._history.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this._history.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype.back = function (cb) {
|
|
||||||
if (this._history.length <= 1) return
|
|
||||||
|
|
||||||
var page = this._history.pop()
|
|
||||||
|
|
||||||
if (page.onbeforeunload) {
|
|
||||||
// TODO: this is buggy. If the user clicks back twice, then those pages
|
|
||||||
// may end up in _forward in the wrong order depending on which onbeforeunload
|
|
||||||
// call finishes first.
|
|
||||||
page.onbeforeunload(() => {
|
|
||||||
this._forward.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this._forward.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype.forward = function (cb) {
|
|
||||||
if (this._forward.length === 0) return
|
|
||||||
|
|
||||||
var page = this._forward.pop()
|
|
||||||
this._go(page, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype.clearForward = function () {
|
|
||||||
this._forward = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.current = function () {
|
LocationHistory.prototype.current = function () {
|
||||||
return this._history[this._history.length - 1]
|
return this._history[this._history.length - 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.go = function (page, cb) {
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (this._pending) return cb(null)
|
||||||
|
|
||||||
|
console.log('go', page)
|
||||||
|
|
||||||
|
this.clearForward()
|
||||||
|
this._go(page, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.back = function (cb) {
|
||||||
|
var self = this
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (self._history.length <= 1 || self._pending) return cb(null)
|
||||||
|
|
||||||
|
var page = self._history.pop()
|
||||||
|
self._unload(page, done)
|
||||||
|
|
||||||
|
function done (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
self._forward.push(page)
|
||||||
|
self._load(self.current(), cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.hasBack = function () {
|
LocationHistory.prototype.hasBack = function () {
|
||||||
return this._history.length > 1
|
return this._history.length > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.forward = function (cb) {
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (this._forward.length === 0 || this._pending) return cb(null)
|
||||||
|
|
||||||
|
var page = this._forward.pop()
|
||||||
|
this._go(page, cb)
|
||||||
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.hasForward = function () {
|
LocationHistory.prototype.hasForward = function () {
|
||||||
return this._forward.length > 0
|
return this._forward.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.pending = function () {
|
LocationHistory.prototype.clearForward = function (url) {
|
||||||
return this._pending
|
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 () {
|
LocationHistory.prototype.backToFirst = function (cb) {
|
||||||
this._pending = null
|
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 () {}
|
||||||
|
|||||||
@@ -6,41 +6,43 @@ module.exports = {
|
|||||||
var config = require('../../config')
|
var config = require('../../config')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
|
var VOLUME = 0.15
|
||||||
|
|
||||||
/* Cache of Audio elements, for instant playback */
|
/* Cache of Audio elements, for instant playback */
|
||||||
var cache = {}
|
var cache = {}
|
||||||
|
|
||||||
var sounds = {
|
var sounds = {
|
||||||
ADD: {
|
ADD: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
||||||
volume: 0.1
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
DISABLE: {
|
DISABLE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
DONE: {
|
DONE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
ENABLE: {
|
ENABLE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
ERROR: {
|
ERROR: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
PLAY: {
|
PLAY: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
|
||||||
volume: 0.2
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
STARTUP: {
|
STARTUP: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
||||||
volume: 0.4
|
volume: VOLUME * 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,27 @@ function isPlayable (file) {
|
|||||||
|
|
||||||
function isVideo (file) {
|
function isVideo (file) {
|
||||||
var ext = path.extname(file.name).toLowerCase()
|
var ext = path.extname(file.name).toLowerCase()
|
||||||
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
|
return [
|
||||||
|
'.avi',
|
||||||
|
'.m4v',
|
||||||
|
'.mkv',
|
||||||
|
'.mov',
|
||||||
|
'.mp4',
|
||||||
|
'.mpg',
|
||||||
|
'.ogv',
|
||||||
|
'.webm'
|
||||||
|
].includes(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAudio (file) {
|
function isAudio (file) {
|
||||||
var ext = path.extname(file.name).toLowerCase()
|
var ext = path.extname(file.name).toLowerCase()
|
||||||
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
|
return [
|
||||||
|
'.aac',
|
||||||
|
'.ac3',
|
||||||
|
'.mp3',
|
||||||
|
'.ogg',
|
||||||
|
'.wav'
|
||||||
|
].includes(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlayableTorrent (torrentSummary) {
|
function isPlayableTorrent (torrentSummary) {
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ var captureVideoFrame = require('./capture-video-frame')
|
|||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
function torrentPoster (torrent, cb) {
|
function torrentPoster (torrent, cb) {
|
||||||
// First, try to use 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
|
// Filter out file formats that the <video> tag definitely can't play
|
||||||
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
||||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||||
|
|
||||||
// Second, try to use the largest image file
|
// Third, try to use the largest image file
|
||||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
|
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
|
||||||
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,13 @@ function getInitialState () {
|
|||||||
*
|
*
|
||||||
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
||||||
*/
|
*/
|
||||||
saved: {}
|
saved: {},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Getters, for convenience
|
||||||
|
*/
|
||||||
|
getPlayingTorrentSummary,
|
||||||
|
getPlayingFileSummary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,10 +80,13 @@ function getDefaultPlayState () {
|
|||||||
isStalled: false,
|
isStalled: false,
|
||||||
lastTimeUpdate: 0, /* Unix time in ms */
|
lastTimeUpdate: 0, /* Unix time in ms */
|
||||||
mouseStationarySince: 0, /* Unix time in ms */
|
mouseStationarySince: 0, /* Unix time in ms */
|
||||||
|
playbackRate: 1,
|
||||||
subtitles: {
|
subtitles: {
|
||||||
tracks: [], /* subtitles file (Buffer) */
|
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
||||||
enabled: false
|
selectedIndex: -1, /* current subtitle track */
|
||||||
}
|
showMenu: false /* popover menu, above the video */
|
||||||
|
},
|
||||||
|
aspectRatio: 0 /* aspect ratio of the video */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,8 +266,21 @@ function getDefaultSavedState () {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
downloadPath: config.IS_PORTABLE
|
prefs: {
|
||||||
? path.join(config.CONFIG_PATH, 'Downloads')
|
downloadPath: config.IS_PORTABLE
|
||||||
: remote.app.getPath('downloads')
|
? path.join(config.CONFIG_PATH, 'Downloads')
|
||||||
|
: remote.app.getPath('downloads')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPlayingTorrentSummary () {
|
||||||
|
var infoHash = this.playing.infoHash
|
||||||
|
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayingFileSummary () {
|
||||||
|
var torrentSummary = this.getPlayingTorrentSummary()
|
||||||
|
if (!torrentSummary) return null
|
||||||
|
return torrentSummary.files[this.playing.fileIndex]
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ var Header = require('./header')
|
|||||||
var Views = {
|
var Views = {
|
||||||
'home': require('./torrent-list'),
|
'home': require('./torrent-list'),
|
||||||
'player': require('./player'),
|
'player': require('./player'),
|
||||||
'create-torrent': require('./create-torrent-page')
|
'create-torrent': require('./create-torrent-page'),
|
||||||
|
'preferences': require('./preferences')
|
||||||
}
|
}
|
||||||
var Modals = {
|
var Modals = {
|
||||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||||
'update-available-modal': require('./update-available-modal')
|
'update-available-modal': require('./update-available-modal'),
|
||||||
|
'unsupported-media-modal': require('./unsupported-media-modal')
|
||||||
}
|
}
|
||||||
|
|
||||||
function App (state) {
|
function App (state) {
|
||||||
@@ -21,24 +23,20 @@ function App (state) {
|
|||||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||||
// * The video is paused
|
// * The video is paused
|
||||||
// * The video is playing remotely on Chromecast or Airplay
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
var hideControls = state.location.current().url === 'player' &&
|
var hideControls = state.location.url() === 'player' &&
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
state.playing.mouseStationarySince !== 0 &&
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||||
!state.playing.isPaused &&
|
!state.playing.isPaused &&
|
||||||
state.playing.location === 'local'
|
state.playing.location === 'local' &&
|
||||||
|
state.playing.playbackRate === 1
|
||||||
// Hide the header on Windows/Linux when in the player
|
|
||||||
// On OSX, the header appears as part of the title bar
|
|
||||||
var hideHeader = process.platform !== 'darwin' && state.location.current().url === 'player'
|
|
||||||
|
|
||||||
var cls = [
|
var cls = [
|
||||||
'view-' + state.location.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 */
|
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
||||||
]
|
]
|
||||||
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
||||||
if (state.window.isFocused) cls.push('is-focused')
|
if (state.window.isFocused) cls.push('is-focused')
|
||||||
if (hideControls) cls.push('hide-video-controls')
|
if (hideControls) cls.push('hide-video-controls')
|
||||||
if (hideHeader) cls.push('hide-header')
|
|
||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='app ${cls.join(' ')}'>
|
<div class='app ${cls.join(' ')}'>
|
||||||
@@ -53,12 +51,13 @@ function App (state) {
|
|||||||
function getErrorPopover (state) {
|
function getErrorPopover (state) {
|
||||||
var now = new Date().getTime()
|
var now = new Date().getTime()
|
||||||
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
||||||
|
var hasErrors = recentErrors.length > 0
|
||||||
|
|
||||||
var errorElems = recentErrors.map(function (error) {
|
var errorElems = recentErrors.map(function (error) {
|
||||||
return hx`<div class='error'>${error.message}</div>`
|
return hx`<div class='error'>${error.message}</div>`
|
||||||
})
|
})
|
||||||
return hx`
|
return hx`
|
||||||
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
|
<div class='error-popover ${hasErrors ? 'visible' : 'hidden'}'>
|
||||||
<div class='title'>Error</div>
|
<div class='title'>Error</div>
|
||||||
${errorElems}
|
${errorElems}
|
||||||
</div>
|
</div>
|
||||||
@@ -79,6 +78,6 @@ function getModal (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getView (state) {
|
function getView (state) {
|
||||||
var url = state.location.current().url
|
var url = state.location.url()
|
||||||
return Views[url](state)
|
return Views[url](state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ var createTorrent = require('create-torrent')
|
|||||||
var path = require('path')
|
var path = require('path')
|
||||||
var prettyBytes = require('prettier-bytes')
|
var prettyBytes = require('prettier-bytes')
|
||||||
|
|
||||||
var {dispatch} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
function CreateTorrentPage (state) {
|
function CreateTorrentPage (state) {
|
||||||
var info = state.location.current()
|
var info = state.location.current()
|
||||||
@@ -17,23 +17,19 @@ function CreateTorrentPage (state) {
|
|||||||
var files = info.files
|
var files = info.files
|
||||||
.filter((f) => !f.name.startsWith('.'))
|
.filter((f) => !f.name.startsWith('.'))
|
||||||
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
||||||
|
if (files.length === 0) return CreateTorrentErrorPage()
|
||||||
|
|
||||||
// First, extract the base folder that the files are all in
|
// First, extract the base folder that the files are all in
|
||||||
var pathPrefix = info.folderPath
|
var pathPrefix = info.folderPath
|
||||||
if (!pathPrefix) {
|
if (!pathPrefix) {
|
||||||
if (files.length > 0) {
|
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||||
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
pathPrefix = path.dirname(pathPrefix)
|
||||||
pathPrefix = path.dirname(pathPrefix)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pathPrefix = files[0]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check: show the number of files and total size
|
// Sanity check: show the number of files and total size
|
||||||
var numFiles = files.length
|
var numFiles = files.length
|
||||||
console.log('FILES', files)
|
|
||||||
var totalBytes = files
|
var totalBytes = files
|
||||||
.map((f) => f.size)
|
.map((f) => f.size)
|
||||||
.reduce((a, b) => a + b, 0)
|
.reduce((a, b) => a + b, 0)
|
||||||
@@ -41,8 +37,16 @@ function CreateTorrentPage (state) {
|
|||||||
|
|
||||||
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
||||||
// as the default name. Show all files relative to the base folder.
|
// as the default name. Show all files relative to the base folder.
|
||||||
var defaultName = path.basename(pathPrefix)
|
var defaultName, basePath
|
||||||
var basePath = path.dirname(pathPrefix)
|
if (files.length === 1) {
|
||||||
|
// Single file torrent: /a/b/foo.jpg -> torrent name "foo.jpg", path "/a/b"
|
||||||
|
defaultName = files[0].name
|
||||||
|
basePath = pathPrefix
|
||||||
|
} else {
|
||||||
|
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a"
|
||||||
|
defaultName = path.basename(pathPrefix)
|
||||||
|
basePath = path.dirname(pathPrefix)
|
||||||
|
}
|
||||||
var maxFileElems = 100
|
var maxFileElems = 100
|
||||||
var fileElems = files.slice(0, maxFileElems).map(function (file) {
|
var fileElems = files.slice(0, maxFileElems).map(function (file) {
|
||||||
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
|
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
|
||||||
@@ -112,11 +116,10 @@ function CreateTorrentPage (state) {
|
|||||||
comment: comment
|
comment: comment
|
||||||
}
|
}
|
||||||
dispatch('createTorrent', options)
|
dispatch('createTorrent', options)
|
||||||
dispatch('backToList')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel () {
|
function handleCancel () {
|
||||||
dispatch('backToList')
|
dispatch('back')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleShowAdvanced () {
|
function handleToggleShowAdvanced () {
|
||||||
@@ -127,6 +130,27 @@ function CreateTorrentPage (state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CreateTorrentErrorPage () {
|
||||||
|
return hx`
|
||||||
|
<div class='create-torrent-page'>
|
||||||
|
<h2>Create torrent</h2>
|
||||||
|
<p class="torrent-info">
|
||||||
|
<p>
|
||||||
|
Sorry, you must select at least one file that is not a hidden file.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Hidden files, starting with a . character, are not included.
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
<p class="float-right">
|
||||||
|
<button class='button-flat light' onclick=${dispatcher('back')}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
// Finds the longest common prefix
|
// Finds the longest common prefix
|
||||||
function findCommonPrefix (a, b) {
|
function findCommonPrefix (a, b) {
|
||||||
for (var i = 0; i < a.length && i < b.length; i++) {
|
for (var i = 0; i < a.length && i < b.length; i++) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function Header (state) {
|
|||||||
return hx`
|
return hx`
|
||||||
<div class='header'>
|
<div class='header'>
|
||||||
${getTitle()}
|
${getTitle()}
|
||||||
<div class='nav left'>
|
<div class='nav left float-left'>
|
||||||
<i.icon.back
|
<i.icon.back
|
||||||
class=${state.location.hasBack() ? '' : 'disabled'}
|
class=${state.location.hasBack() ? '' : 'disabled'}
|
||||||
title='Back'
|
title='Back'
|
||||||
@@ -24,7 +24,7 @@ function Header (state) {
|
|||||||
chevron_right
|
chevron_right
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
<div class='nav right'>
|
<div class='nav right float-right'>
|
||||||
${getAddButton()}
|
${getAddButton()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,7 @@ function Header (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAddButton () {
|
function getAddButton () {
|
||||||
if (state.location.current().url !== 'player') {
|
if (state.location.url() === 'home') {
|
||||||
return hx`
|
return hx`
|
||||||
<i
|
<i
|
||||||
class='icon add'
|
class='icon add'
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ var h = require('virtual-dom/h')
|
|||||||
var hyperx = require('hyperx')
|
var hyperx = require('hyperx')
|
||||||
var hx = hyperx(h)
|
var hx = hyperx(h)
|
||||||
|
|
||||||
var prettyBytes = require('prettier-bytes')
|
|
||||||
var Bitfield = require('bitfield')
|
var Bitfield = require('bitfield')
|
||||||
|
var prettyBytes = require('prettier-bytes')
|
||||||
|
var zeroFill = require('zero-fill')
|
||||||
|
|
||||||
var TorrentSummary = require('../lib/torrent-summary')
|
var TorrentSummary = require('../lib/torrent-summary')
|
||||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
@@ -18,6 +19,7 @@ function Player (state) {
|
|||||||
return hx`
|
return hx`
|
||||||
<div
|
<div
|
||||||
class='player'
|
class='player'
|
||||||
|
onwheel=${handleVolumeWheel}
|
||||||
onmousemove=${dispatcher('mediaMouseMoved')}>
|
onmousemove=${dispatcher('mediaMouseMoved')}>
|
||||||
${showVideo ? renderMedia(state) : renderCastScreen(state)}
|
${showVideo ? renderMedia(state) : renderCastScreen(state)}
|
||||||
${renderPlayerControls(state)}
|
${renderPlayerControls(state)}
|
||||||
@@ -25,12 +27,18 @@ function Player (state) {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles volume change by wheel
|
||||||
|
function handleVolumeWheel (e) {
|
||||||
|
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
|
||||||
|
}
|
||||||
|
|
||||||
function renderMedia (state) {
|
function renderMedia (state) {
|
||||||
if (!state.server) return
|
if (!state.server) return
|
||||||
|
|
||||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||||
// Instead, grab the DOM node and play/pause it if necessary
|
// Instead, grab the DOM node and play/pause it if necessary
|
||||||
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
|
// Get the <video> or <audio> tag
|
||||||
|
var mediaElement = document.querySelector(state.playing.type)
|
||||||
if (mediaElement !== null) {
|
if (mediaElement !== null) {
|
||||||
if (state.playing.isPaused && !mediaElement.paused) {
|
if (state.playing.isPaused && !mediaElement.paused) {
|
||||||
mediaElement.pause()
|
mediaElement.pause()
|
||||||
@@ -42,36 +50,38 @@ function renderMedia (state) {
|
|||||||
mediaElement.currentTime = state.playing.jumpToTime
|
mediaElement.currentTime = state.playing.jumpToTime
|
||||||
state.playing.jumpToTime = null
|
state.playing.jumpToTime = null
|
||||||
}
|
}
|
||||||
|
if (state.playing.playbackRate !== mediaElement.playbackRate) {
|
||||||
|
mediaElement.playbackRate = state.playing.playbackRate
|
||||||
|
}
|
||||||
// Set volume
|
// Set volume
|
||||||
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
||||||
mediaElement.volume = state.playing.setVolume
|
mediaElement.volume = state.playing.setVolume
|
||||||
state.playing.setVolume = null
|
state.playing.setVolume = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix textTrack cues not been removed <track> rerender
|
// Switch to the newly added subtitle track, if available
|
||||||
if (state.playing.subtitles.change) {
|
var tracks = mediaElement.textTracks
|
||||||
var tracks = mediaElement.textTracks
|
for (var j = 0; j < tracks.length; j++) {
|
||||||
for (var j = 0; j < tracks.length; j++) {
|
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
||||||
// mode is not an <track> attribute, only available on DOM
|
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
|
||||||
tracks[j].mode = (tracks[j].label === state.playing.subtitles.change) ? 'showing' : 'hidden'
|
|
||||||
}
|
|
||||||
state.playing.subtitles.change = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.playing.currentTime = mediaElement.currentTime
|
// Save video position
|
||||||
state.playing.duration = mediaElement.duration
|
var file = state.getPlayingFileSummary()
|
||||||
|
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
||||||
|
file.duration = state.playing.duration = mediaElement.duration
|
||||||
state.playing.volume = mediaElement.volume
|
state.playing.volume = mediaElement.volume
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add subtitles to the <video> tag
|
// Add subtitles to the <video> tag
|
||||||
var trackTags = []
|
var trackTags = []
|
||||||
|
if (state.playing.subtitles.selectedIndex >= 0) {
|
||||||
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
|
|
||||||
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||||
var track = state.playing.subtitles.tracks[i]
|
var track = state.playing.subtitles.tracks[i]
|
||||||
|
var isSelected = state.playing.subtitles.selectedIndex === i
|
||||||
trackTags.push(hx`
|
trackTags.push(hx`
|
||||||
<track
|
<track
|
||||||
${track.selected ? 'default' : ''}
|
${isSelected ? 'default' : ''}
|
||||||
label=${track.label}
|
label=${track.label}
|
||||||
type='subtitles'
|
type='subtitles'
|
||||||
src=${track.buffer}>
|
src=${track.buffer}>
|
||||||
@@ -89,7 +99,8 @@ function renderMedia (state) {
|
|||||||
onstalling=${dispatcher('mediaStalled')}
|
onstalling=${dispatcher('mediaStalled')}
|
||||||
onerror=${dispatcher('mediaError')}
|
onerror=${dispatcher('mediaError')}
|
||||||
ontimeupdate=${dispatcher('mediaTimeUpdate')}
|
ontimeupdate=${dispatcher('mediaTimeUpdate')}
|
||||||
autoplay>
|
onencrypted=${dispatcher('mediaEncrypted')}
|
||||||
|
oncanplay=${onCanPlay}>
|
||||||
${trackTags}
|
${trackTags}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -105,7 +116,7 @@ function renderMedia (state) {
|
|||||||
</div>
|
</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) {
|
function onLoadedMetadata (e) {
|
||||||
if (state.playing.type !== 'video') return
|
if (state.playing.type !== 'video') return
|
||||||
var video = e.target
|
var video = e.target
|
||||||
@@ -120,6 +131,18 @@ function renderMedia (state) {
|
|||||||
function onEnded (e) {
|
function onEnded (e) {
|
||||||
state.playing.isPaused = true
|
state.playing.isPaused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onCanPlay (e) {
|
||||||
|
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 {
|
||||||
|
elem.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOverlay (state) {
|
function renderOverlay (state) {
|
||||||
@@ -137,7 +160,8 @@ function renderOverlay (state) {
|
|||||||
} else if (elems.length !== 0) {
|
} else if (elems.length !== 0) {
|
||||||
style = { backgroundImage: cssBackgroundImageDarkGradient() }
|
style = { backgroundImage: cssBackgroundImageDarkGradient() }
|
||||||
} else {
|
} 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`
|
return hx`
|
||||||
@@ -148,8 +172,7 @@ function renderOverlay (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAudioMetadata (state) {
|
function renderAudioMetadata (state) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var fileSummary = state.getPlayingFileSummary()
|
||||||
var fileSummary = torrentSummary.files[state.playing.fileIndex]
|
|
||||||
if (!fileSummary.audioInfo) return
|
if (!fileSummary.audioInfo) return
|
||||||
var info = fileSummary.audioInfo
|
var info = fileSummary.audioInfo
|
||||||
|
|
||||||
@@ -168,15 +191,37 @@ function renderAudioMetadata (state) {
|
|||||||
track = info.track.no + ' of ' + info.track.of
|
track = info.track.no + ' of ' + info.track.of
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a small info box in the middle of the screen with title/album/artist/etc
|
// Show a small info box in the middle of the screen with title/album/etc
|
||||||
var elems = []
|
var elems = []
|
||||||
if (artist) elems.push(hx`<div class='audio-artist'><label>Artist</label>${artist}</div>`)
|
if (artist) {
|
||||||
if (album) elems.push(hx`<div class='audio-album'><label>Album</label>${album}</div>`)
|
elems.push(hx`
|
||||||
if (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`)
|
<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>`
|
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>`
|
return hx`<div class='audio-metadata'>${elems}</div>`
|
||||||
}
|
}
|
||||||
@@ -187,7 +232,7 @@ function renderLoadingSpinner (state) {
|
|||||||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
||||||
if (!isProbablyStalled) return
|
if (!isProbablyStalled) return
|
||||||
|
|
||||||
var prog = getPlayingTorrentSummary(state).progress || {}
|
var prog = state.getPlayingTorrentSummary().progress || {}
|
||||||
var fileProgress = 0
|
var fileProgress = 0
|
||||||
if (prog.files) {
|
if (prog.files) {
|
||||||
var file = prog.files[state.playing.fileIndex]
|
var file = prog.files[state.playing.fileIndex]
|
||||||
@@ -207,20 +252,33 @@ function renderLoadingSpinner (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderCastScreen (state) {
|
function renderCastScreen (state) {
|
||||||
var castIcon, castType
|
var castIcon, castType, isCast
|
||||||
if (state.playing.location.startsWith('chromecast')) {
|
if (state.playing.location.startsWith('chromecast')) {
|
||||||
castIcon = 'cast_connected'
|
castIcon = 'cast_connected'
|
||||||
castType = 'Chromecast'
|
castType = 'Chromecast'
|
||||||
|
isCast = true
|
||||||
} else if (state.playing.location.startsWith('airplay')) {
|
} else if (state.playing.location.startsWith('airplay')) {
|
||||||
castIcon = 'airplay'
|
castIcon = 'airplay'
|
||||||
castType = 'AirPlay'
|
castType = 'AirPlay'
|
||||||
|
isCast = true
|
||||||
} else if (state.playing.location.startsWith('dlna')) {
|
} else if (state.playing.location.startsWith('dlna')) {
|
||||||
castIcon = 'tv'
|
castIcon = 'tv'
|
||||||
castType = 'DLNA'
|
castType = 'DLNA'
|
||||||
|
isCast = true
|
||||||
|
} else if (state.playing.location === 'vlc') {
|
||||||
|
castIcon = 'tv'
|
||||||
|
castType = 'VLC'
|
||||||
|
isCast = false
|
||||||
|
} else if (state.playing.location === 'error') {
|
||||||
|
castIcon = 'error_outline'
|
||||||
|
castType = 'Error'
|
||||||
|
isCast = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var isStarting = state.playing.location.endsWith('-pending')
|
var isStarting = state.playing.location.endsWith('-pending')
|
||||||
var castStatus = isStarting ? 'Connecting...' : 'Connected'
|
var castStatus
|
||||||
|
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
|
||||||
|
else castStatus = ''
|
||||||
|
|
||||||
// Show a nice title image, if possible
|
// Show a nice title image, if possible
|
||||||
var style = {
|
var style = {
|
||||||
@@ -240,23 +298,37 @@ function renderCastScreen (state) {
|
|||||||
|
|
||||||
function renderSubtitlesOptions (state) {
|
function renderSubtitlesOptions (state) {
|
||||||
var subtitles = state.playing.subtitles
|
var subtitles = state.playing.subtitles
|
||||||
if (subtitles.tracks.length && subtitles.show) {
|
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||||
return hx`<ul.subtitles-list>
|
|
||||||
${subtitles.tracks.map(function (w, i) {
|
var items = subtitles.tracks.map(function (track, ix) {
|
||||||
return hx`<li onclick=${dispatcher('selectSubtitle', w.label)}><i.icon>${w.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>${w.label}</li>`
|
var isSelected = state.playing.subtitles.selectedIndex === ix
|
||||||
})}
|
return hx`
|
||||||
<li onclick=${dispatcher('selectSubtitle', '')}><i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>None</li>
|
<li onclick=${dispatcher('selectSubtitle', ix)}>
|
||||||
</ul>
|
<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>${noneClass}</i>
|
||||||
|
None
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPlayerControls (state) {
|
function renderPlayerControls (state) {
|
||||||
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
var captionsClass = state.playing.subtitles.tracks.length === 0
|
||||||
? 'disabled'
|
? 'disabled'
|
||||||
: state.playing.subtitles.enabled
|
: state.playing.subtitles.selectedIndex >= 0
|
||||||
? 'active'
|
? 'active'
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
@@ -264,15 +336,27 @@ function renderPlayerControls (state) {
|
|||||||
hx`
|
hx`
|
||||||
<div class='playback-bar'>
|
<div class='playback-bar'>
|
||||||
${renderLoadingBar(state)}
|
${renderLoadingBar(state)}
|
||||||
<div class='playback-cursor' style=${playbackCursorStyle}></div>
|
<div
|
||||||
<div class='scrub-bar'
|
class='playback-cursor'
|
||||||
|
style=${playbackCursorStyle}>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class='scrub-bar'
|
||||||
draggable='true'
|
draggable='true'
|
||||||
|
ondragstart=${handleDragStart}
|
||||||
onclick=${handleScrub},
|
onclick=${handleScrub},
|
||||||
ondrag=${handleScrub}></div>
|
ondrag=${handleScrub}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
hx`
|
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')}>
|
onclick=${dispatcher('toggleFullScreen')}>
|
||||||
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||||
</i>
|
</i>
|
||||||
@@ -282,7 +366,7 @@ function renderPlayerControls (state) {
|
|||||||
if (state.playing.type === 'video') {
|
if (state.playing.type === 'video') {
|
||||||
// show closed captions icon
|
// show closed captions icon
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.closed-captions
|
<i.icon.closed-captions.float-right
|
||||||
class=${captionsClass}
|
class=${captionsClass}
|
||||||
onclick=${handleSubtitles}>
|
onclick=${handleSubtitles}>
|
||||||
closed_captions
|
closed_captions
|
||||||
@@ -294,7 +378,9 @@ function renderPlayerControls (state) {
|
|||||||
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
||||||
var isOnAirplay = state.playing.location.startsWith('airplay')
|
var isOnAirplay = state.playing.location.startsWith('airplay')
|
||||||
var isOnDlna = state.playing.location.startsWith('dlna')
|
var isOnDlna = state.playing.location.startsWith('dlna')
|
||||||
var chromecastClass, chromecastHandler, airplayClass, airplayHandler, dlnaClass, dlnaHandler
|
var chromecastClass, chromecastHandler
|
||||||
|
var airplayClass, airplayHandler
|
||||||
|
var dlnaClass, dlnaHandler
|
||||||
if (isOnChromecast) {
|
if (isOnChromecast) {
|
||||||
chromecastClass = 'active'
|
chromecastClass = 'active'
|
||||||
dlnaClass = 'disabled'
|
dlnaClass = 'disabled'
|
||||||
@@ -327,7 +413,7 @@ function renderPlayerControls (state) {
|
|||||||
if (state.devices.chromecast || isOnChromecast) {
|
if (state.devices.chromecast || isOnChromecast) {
|
||||||
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
|
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.device
|
<i.icon.device.float-right
|
||||||
class=${chromecastClass}
|
class=${chromecastClass}
|
||||||
onclick=${chromecastHandler}>
|
onclick=${chromecastHandler}>
|
||||||
${castIcon}
|
${castIcon}
|
||||||
@@ -336,7 +422,7 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
if (state.devices.airplay || isOnAirplay) {
|
if (state.devices.airplay || isOnAirplay) {
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.device
|
<i.icon.device.float-right
|
||||||
class=${airplayClass}
|
class=${airplayClass}
|
||||||
onclick=${airplayHandler}>
|
onclick=${airplayHandler}>
|
||||||
airplay
|
airplay
|
||||||
@@ -345,7 +431,8 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
if (state.devices.dlna || isOnDlna) {
|
if (state.devices.dlna || isOnDlna) {
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.device
|
<i
|
||||||
|
class='icon device float-right'
|
||||||
class=${dlnaClass}
|
class=${dlnaClass}
|
||||||
onclick=${dlnaHandler}>
|
onclick=${dlnaHandler}>
|
||||||
tv
|
tv
|
||||||
@@ -353,56 +440,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
|
// render volume
|
||||||
var volume = state.playing.volume
|
var volume = state.playing.volume
|
||||||
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
|
var volumeIcon = 'volume_' + (
|
||||||
var volumeStyle = { background: '-webkit-gradient(linear, left top, right top, ' +
|
volume === 0 ? 'off'
|
||||||
'color-stop(' + (volume * 100) + '%, #eee), ' +
|
: volume < 0.3 ? 'mute'
|
||||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
: 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`
|
elements.push(hx`
|
||||||
<div.volume
|
<div class='volume float-left'>
|
||||||
onwheel=${handleVolumeWheel}>
|
<i
|
||||||
<i.icon.volume-icon onmousedown=${handleVolumeMute}>
|
class='icon volume-icon float-left'
|
||||||
${volumeIcon}
|
onmousedown=${handleVolumeMute}>
|
||||||
</i>
|
${volumeIcon}
|
||||||
<input.volume-slider
|
</i>
|
||||||
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
|
<input
|
||||||
onmousedown=${handleVolumeScrub}
|
class='volume-slider float-right'
|
||||||
onmouseup=${handleVolumeScrub}
|
type='range' min='0' max='1' step='0.05'
|
||||||
onmousemove=${handleVolumeScrub}
|
value=${volumeChanging !== false ? volumeChanging : volume}
|
||||||
onwheel=${handleVolumeWheel}
|
onmousedown=${handleVolumeScrub}
|
||||||
style=${volumeStyle}
|
onmouseup=${handleVolumeScrub}
|
||||||
/>
|
onmousemove=${handleVolumeScrub}
|
||||||
|
style=${volumeStyle}
|
||||||
|
/>
|
||||||
</div>
|
</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`
|
elements.push(hx`
|
||||||
<i class='icon play-pause' onclick=${dispatcher('playPause')}>
|
<span class='time float-left'>
|
||||||
${state.playing.isPaused ? 'play_arrow' : 'pause'}
|
${currentTimeStr} / ${durationStr}
|
||||||
</i>
|
</span>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// render playback rate
|
||||||
|
if (state.playing.playbackRate !== 1) {
|
||||||
|
elements.push(hx`
|
||||||
|
<span class='rate float-left'>
|
||||||
|
${state.playing.playbackRate}x
|
||||||
|
</span>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='player-controls'>
|
<div class='controls'>
|
||||||
${elements}
|
${elements}
|
||||||
${renderSubtitlesOptions(state)}
|
${renderSubtitlesOptions(state)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
|
function handleDragStart (e) {
|
||||||
|
// Prevent the cursor from changing, eg to a green + icon on Mac
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
var dt = e.dataTransfer
|
||||||
|
dt.effectAllowed = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handles a click or drag to scrub (jump to another position in the video)
|
// Handles a click or drag to scrub (jump to another position in the video)
|
||||||
function handleScrub (e) {
|
function handleScrub (e) {
|
||||||
dispatch('mediaMouseMoved')
|
dispatch('mediaMouseMoved')
|
||||||
@@ -412,11 +514,6 @@ function renderPlayerControls (state) {
|
|||||||
dispatch('playbackJump', position)
|
dispatch('playbackJump', position)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles volume change by wheel
|
|
||||||
function handleVolumeWheel (e) {
|
|
||||||
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles volume muting and Unmuting
|
// Handles volume muting and Unmuting
|
||||||
function handleVolumeMute (e) {
|
function handleVolumeMute (e) {
|
||||||
if (state.playing.volume === 0.0) {
|
if (state.playing.volume === 0.0) {
|
||||||
@@ -450,7 +547,7 @@ function renderPlayerControls (state) {
|
|||||||
// if no subtitles available select it
|
// if no subtitles available select it
|
||||||
dispatch('openSubtitles')
|
dispatch('openSubtitles')
|
||||||
} else {
|
} else {
|
||||||
dispatch('showSubtitles')
|
dispatch('toggleSubtitlesMenu')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,7 +558,7 @@ var volumeChanging = false
|
|||||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||||
// can be "spongey" / non-contiguous
|
// can be "spongey" / non-contiguous
|
||||||
function renderLoadingBar (state) {
|
function renderLoadingBar (state) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary.progress) {
|
if (!torrentSummary.progress) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -498,7 +595,7 @@ function renderLoadingBar (state) {
|
|||||||
|
|
||||||
// Returns the CSS background-image string for a poster image + dark vignette
|
// Returns the CSS background-image string for a poster image + dark vignette
|
||||||
function cssBackgroundImagePoster (state) {
|
function cssBackgroundImagePoster (state) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
if (!posterPath) return ''
|
if (!posterPath) return ''
|
||||||
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
||||||
@@ -509,7 +606,17 @@ function cssBackgroundImageDarkGradient () {
|
|||||||
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayingTorrentSummary (state) {
|
function formatTime (time) {
|
||||||
var infoHash = state.playing.infoHash
|
if (typeof time !== 'number' || Number.isNaN(time)) {
|
||||||
return state.saved.torrents.find((x) => x.infoHash === infoHash)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
104
renderer/views/preferences.js
Normal file
104
renderer/views/preferences.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
module.exports = Preferences
|
||||||
|
|
||||||
|
var h = require('virtual-dom/h')
|
||||||
|
var hyperx = require('hyperx')
|
||||||
|
var hx = hyperx(h)
|
||||||
|
var {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
var remote = require('electron').remote
|
||||||
|
var dialog = remote.dialog
|
||||||
|
|
||||||
|
function Preferences (state) {
|
||||||
|
return hx`
|
||||||
|
<div class='preferences'>
|
||||||
|
${renderGeneralSection(state)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGeneralSection (state) {
|
||||||
|
return renderSection({
|
||||||
|
title: 'General',
|
||||||
|
description: '',
|
||||||
|
icon: 'settings'
|
||||||
|
}, [
|
||||||
|
renderDownloadDirSelector(state)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDownloadDirSelector (state) {
|
||||||
|
return renderFileSelector({
|
||||||
|
label: 'Download Path',
|
||||||
|
description: 'Data from torrents will be saved here',
|
||||||
|
property: 'downloadPath',
|
||||||
|
options: {
|
||||||
|
title: 'Select download directory',
|
||||||
|
properties: [ 'openDirectory' ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state.unsaved.prefs.downloadPath,
|
||||||
|
function (filePath) {
|
||||||
|
setStateValue('downloadPath', filePath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders a prefs section.
|
||||||
|
// - definition should be {icon, title, description}
|
||||||
|
// - controls should be an array of vdom elements
|
||||||
|
function renderSection (definition, controls) {
|
||||||
|
var helpElem = !definition.description ? null : hx`
|
||||||
|
<div class='help text'>
|
||||||
|
<i.icon>help_outline</i>${definition.description}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
return hx`
|
||||||
|
<section class='section preferences-panel'>
|
||||||
|
<div class='section-container'>
|
||||||
|
<div class='section-heading'>
|
||||||
|
<i.icon>${definition.icon}</i>${definition.title}
|
||||||
|
</div>
|
||||||
|
${helpElem}
|
||||||
|
<div class='section-body'>
|
||||||
|
${controls}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a file chooser
|
||||||
|
// - defition should be {label, description, options}
|
||||||
|
// options are passed to dialog.showOpenDialog
|
||||||
|
// - value should be the current pref, a file or folder path
|
||||||
|
// - callback takes a new file or folder path
|
||||||
|
function renderFileSelector (definition, value, callback) {
|
||||||
|
return hx`
|
||||||
|
<div class='control-group'>
|
||||||
|
<div class='controls'>
|
||||||
|
<label class='control-label'>
|
||||||
|
<div class='preference-title'>${definition.label}</div>
|
||||||
|
<div class='preference-description'>${definition.description}</div>
|
||||||
|
</label>
|
||||||
|
<div class='controls'>
|
||||||
|
<input type='text' class='file-picker-text'
|
||||||
|
id=${definition.property}
|
||||||
|
disabled='disabled'
|
||||||
|
value=${value} />
|
||||||
|
<button class='btn' onclick=${handleClick}>
|
||||||
|
<i.icon>folder_open</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
function handleClick () {
|
||||||
|
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
||||||
|
if (!Array.isArray(filenames)) return
|
||||||
|
callback(filenames[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStateValue (property, value) {
|
||||||
|
dispatch('updatePreferences', property, value)
|
||||||
|
}
|
||||||
@@ -67,39 +67,49 @@ function TorrentList (state) {
|
|||||||
// If it's downloading/seeding then show progress info
|
// If it's downloading/seeding then show progress info
|
||||||
var prog = torrentSummary.progress
|
var prog = torrentSummary.progress
|
||||||
if (torrentSummary.status !== 'paused' && prog) {
|
if (torrentSummary.status !== 'paused' && prog) {
|
||||||
var progress = Math.floor(100 * prog.progress)
|
|
||||||
var downloaded = prettyBytes(prog.downloaded)
|
|
||||||
var total = prettyBytes(prog.length || 0)
|
|
||||||
if (downloaded !== total) downloaded += ` / ${total}`
|
|
||||||
|
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<div class='status ellipsis'>
|
<div class='ellipsis'>
|
||||||
${getFilesLength()}
|
${renderPercentProgress()}
|
||||||
<span>${getPeers()}</span>
|
${renderTotalProgress()}
|
||||||
<span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
|
${renderPeers()}
|
||||||
<span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
|
${renderDownloadSpeed()}
|
||||||
</div>
|
${renderUploadSpeed()}
|
||||||
`)
|
|
||||||
elements.push(hx`
|
|
||||||
<div class='status2 ellipsis'>
|
|
||||||
<span class='progress'>${progress}%</span>
|
|
||||||
<span>${downloaded}</span>
|
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return hx`<div class='metadata'>${elements}</div>`
|
return hx`<div class='metadata'>${elements}</div>`
|
||||||
|
|
||||||
function getPeers () {
|
function renderPercentProgress () {
|
||||||
var count = prog.numPeers === 1 ? 'peer' : 'peers'
|
var progress = Math.floor(100 * prog.progress)
|
||||||
return `${prog.numPeers} ${count}`
|
return hx`<span>${progress}%</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilesLength () {
|
function renderTotalProgress () {
|
||||||
if (torrentSummary.files && torrentSummary.files.length > 1) {
|
var downloaded = prettyBytes(prog.downloaded)
|
||||||
return hx`<span class='files'>${torrentSummary.files.length} files</span>`
|
var total = prettyBytes(prog.length || 0)
|
||||||
|
if (downloaded === total) {
|
||||||
|
return hx`<span>${downloaded}</span>`
|
||||||
|
} else {
|
||||||
|
return hx`<span>${downloaded} / ${total}</span>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPeers () {
|
||||||
|
if (prog.numPeers === 0) return
|
||||||
|
var count = prog.numPeers === 1 ? 'peer' : 'peers'
|
||||||
|
return hx`<span>${prog.numPeers} ${count}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDownloadSpeed () {
|
||||||
|
if (prog.downloadSpeed === 0) return
|
||||||
|
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUploadSpeed () {
|
||||||
|
if (prog.uploadSpeed === 0) return
|
||||||
|
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download button toggles between torrenting (DL/seed) and paused
|
// Download button toggles between torrenting (DL/seed) and paused
|
||||||
@@ -108,12 +118,7 @@ function TorrentList (state) {
|
|||||||
var infoHash = torrentSummary.infoHash
|
var infoHash = torrentSummary.infoHash
|
||||||
|
|
||||||
var playIcon, playTooltip, playClass
|
var playIcon, playTooltip, playClass
|
||||||
if (torrentSummary.playStatus === 'unplayable') {
|
if (torrentSummary.playStatus === 'timeout') {
|
||||||
playIcon = 'play_arrow'
|
|
||||||
playClass = 'disabled'
|
|
||||||
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
|
|
||||||
'View details and click on individual files to open them in another program.'
|
|
||||||
} else if (torrentSummary.playStatus === 'timeout') {
|
|
||||||
playIcon = 'warning'
|
playIcon = 'warning'
|
||||||
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
||||||
} else {
|
} else {
|
||||||
@@ -133,6 +138,18 @@ function TorrentList (state) {
|
|||||||
downloadTooltip = 'Click to start torrenting.'
|
downloadTooltip = 'Click to start torrenting.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do we have a saved position? Show it using a radial progress bar on top
|
||||||
|
// of the play button, unless already showing a spinner there:
|
||||||
|
var positionElem
|
||||||
|
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
||||||
|
var defaultFile = torrentSummary.files &&
|
||||||
|
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
|
||||||
|
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
|
||||||
|
var fraction = defaultFile.currentTime / defaultFile.duration
|
||||||
|
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
|
||||||
|
playClass = 'resume-position'
|
||||||
|
}
|
||||||
|
|
||||||
// Only show the play button for torrents that contain playable media
|
// Only show the play button for torrents that contain playable media
|
||||||
var playButton
|
var playButton
|
||||||
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
||||||
@@ -148,6 +165,7 @@ function TorrentList (state) {
|
|||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
|
${positionElem}
|
||||||
${playButton}
|
${playButton}
|
||||||
<i.button-round.icon.download
|
<i.button-round.icon.download
|
||||||
class=${torrentSummary.status}
|
class=${torrentSummary.status}
|
||||||
@@ -167,7 +185,6 @@ function TorrentList (state) {
|
|||||||
|
|
||||||
// Show files, per-file download status and play buttons, and so on
|
// Show files, per-file download status and play buttons, and so on
|
||||||
function renderTorrentDetails (torrentSummary) {
|
function renderTorrentDetails (torrentSummary) {
|
||||||
var infoHash = torrentSummary.infoHash
|
|
||||||
var filesElement
|
var filesElement
|
||||||
if (!torrentSummary.files) {
|
if (!torrentSummary.files) {
|
||||||
// We don't know what files this torrent contains
|
// We don't know what files this torrent contains
|
||||||
@@ -177,15 +194,16 @@ function TorrentList (state) {
|
|||||||
filesElement = hx`<div class='files warning'>${message}</div>`
|
filesElement = hx`<div class='files warning'>${message}</div>`
|
||||||
} else {
|
} else {
|
||||||
// We do know the files. List them and show download stats for each one
|
// We do know the files. List them and show download stats for each one
|
||||||
var fileRows = torrentSummary.files.map(
|
var fileRows = torrentSummary.files
|
||||||
(file, index) => renderFileRow(torrentSummary, file, index))
|
.sort(function (a, b) {
|
||||||
|
if (a.name < b.name) return -1
|
||||||
|
if (b.name < a.name) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
.map((file, index) => renderFileRow(torrentSummary, file, index))
|
||||||
|
|
||||||
filesElement = hx`
|
filesElement = hx`
|
||||||
<div class='files'>
|
<div class='files'>
|
||||||
<strong>Files</strong>
|
|
||||||
<span class='open-folder'
|
|
||||||
onclick=${dispatcher('openFolder', infoHash)}>
|
|
||||||
Open folder
|
|
||||||
</span>
|
|
||||||
<table>
|
<table>
|
||||||
${fileRows}
|
${fileRows}
|
||||||
</table>
|
</table>
|
||||||
@@ -203,7 +221,8 @@ function TorrentList (state) {
|
|||||||
// Show a single torrentSummary file in the details view for a single torrent
|
// Show a single torrentSummary file in the details view for a single torrent
|
||||||
function renderFileRow (torrentSummary, file, index) {
|
function renderFileRow (torrentSummary, file, index) {
|
||||||
// First, find out how much of the file we've downloaded
|
// First, find out how much of the file we've downloaded
|
||||||
var isDone = false
|
var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
|
||||||
|
var isDone = false // Are we finished torrenting it?
|
||||||
var progress = ''
|
var progress = ''
|
||||||
if (torrentSummary.progress && torrentSummary.progress.files) {
|
if (torrentSummary.progress && torrentSummary.progress.files) {
|
||||||
var fileProg = torrentSummary.progress.files[index]
|
var fileProg = torrentSummary.progress.files[index]
|
||||||
@@ -211,28 +230,69 @@ function TorrentList (state) {
|
|||||||
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
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 infoHash = torrentSummary.infoHash
|
||||||
var icon
|
var icon
|
||||||
var rowClass = ''
|
|
||||||
var handleClick
|
var handleClick
|
||||||
if (TorrentPlayer.isPlayable(file)) {
|
if (isPlayable) {
|
||||||
icon = 'play_arrow' /* playable? add option to play */
|
icon = 'play_arrow' /* playable? add option to play */
|
||||||
handleClick = dispatcher('play', infoHash, index)
|
handleClick = dispatcher('play', infoHash, index)
|
||||||
} else {
|
} else {
|
||||||
icon = 'description' /* file icon, opens in OS default app */
|
icon = 'description' /* file icon, opens in OS default app */
|
||||||
rowClass = isDone ? '' : 'disabled'
|
|
||||||
handleClick = dispatcher('openFile', infoHash, index)
|
handleClick = dispatcher('openFile', infoHash, index)
|
||||||
}
|
}
|
||||||
|
var rowClass = ''
|
||||||
|
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||||
|
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||||
return hx`
|
return hx`
|
||||||
<tr onclick=${handleClick} class='${rowClass}'>
|
<tr onclick=${handleClick}>
|
||||||
<td class='col-icon'>
|
<td class='col-icon ${rowClass}'>
|
||||||
|
${positionElem}
|
||||||
<i class='icon'>${icon}</i>
|
<i class='icon'>${icon}</i>
|
||||||
</td>
|
</td>
|
||||||
<td class='col-name'>${file.name}</td>
|
<td class='col-name ${rowClass}'>
|
||||||
<td class='col-progress'>${progress}</td>
|
${file.name}
|
||||||
<td class='col-size'>${prettyBytes(file.length)}</td>
|
</td>
|
||||||
|
<td class='col-progress ${rowClass}'>
|
||||||
|
${isSelected ? progress : ''}
|
||||||
|
</td>
|
||||||
|
<td class='col-size ${rowClass}'>
|
||||||
|
${prettyBytes(file.length)}
|
||||||
|
</td>
|
||||||
|
<td class='col-select'
|
||||||
|
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||||
|
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRadialProgressBar (fraction, cssClass) {
|
||||||
|
var rotation = 360 * fraction
|
||||||
|
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
||||||
|
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
||||||
|
|
||||||
|
return hx`
|
||||||
|
<div class="radial-progress ${cssClass}">
|
||||||
|
<div class="circle">
|
||||||
|
<div class="mask full" style=${transformFill}>
|
||||||
|
<div class="fill" style=${transformFill}></div>
|
||||||
|
</div>
|
||||||
|
<div class="mask half">
|
||||||
|
<div class="fill" style=${transformFill}></div>
|
||||||
|
<div class="fill fix" style=${transformFix}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inset"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|||||||
42
renderer/views/unsupported-media-modal.js
Normal file
42
renderer/views/unsupported-media-modal.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
module.exports = UnsupportedMediaModal
|
||||||
|
|
||||||
|
var h = require('virtual-dom/h')
|
||||||
|
var hyperx = require('hyperx')
|
||||||
|
var hx = hyperx(h)
|
||||||
|
|
||||||
|
var electron = require('electron')
|
||||||
|
|
||||||
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
function UnsupportedMediaModal (state) {
|
||||||
|
var err = state.modal.error
|
||||||
|
var message = (err && err.getMessage)
|
||||||
|
? err.getMessage()
|
||||||
|
: err
|
||||||
|
var actionButton = state.modal.vlcInstalled
|
||||||
|
? hx`<button class="button-raised" onclick=${onPlay}>Play in VLC</button>`
|
||||||
|
: hx`<button class="button-raised" onclick=${onInstall}>Install VLC</button>`
|
||||||
|
var vlcMessage = state.modal.vlcNotFound
|
||||||
|
? 'Couldn\'t run VLC. Please make sure it\'s installed.'
|
||||||
|
: ''
|
||||||
|
return hx`
|
||||||
|
<div>
|
||||||
|
<p><strong>Sorry, we can't play that file.</strong></p>
|
||||||
|
<p>${message}</p>
|
||||||
|
<p class='float-right'>
|
||||||
|
<button class="button-flat" onclick=${dispatcher('backToList')}>Cancel</button>
|
||||||
|
${actionButton}
|
||||||
|
</p>
|
||||||
|
<p class='error-text'>${vlcMessage}</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
function onInstall () {
|
||||||
|
electron.shell.openExternal('http://www.videolan.org/vlc/')
|
||||||
|
state.modal.vlcInstalled = true // Assume they'll install it successfully
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlay () {
|
||||||
|
dispatch('vlcPlay')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,16 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
|||||||
|
|
||||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||||
// client, as explained here: https://webtorrent.io/faq
|
// client, as explained here: https://webtorrent.io/faq
|
||||||
var client = window.client = new WebTorrent()
|
var client = window.client = new WebTorrent({
|
||||||
|
tracker: {
|
||||||
|
// HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome bug.
|
||||||
|
// Fixed in Chrome 51, so we can remove this hack once Electron updates Chrome.
|
||||||
|
// Issue: https://github.com/feross/webtorrent-desktop/issues/353
|
||||||
|
// HACK #2: Windows: Disable WebRTC to fix Chrome 50 / Electron 1.1.[1-3] crash.
|
||||||
|
// Issue: https://github.com/electron/electron/issues/5629
|
||||||
|
wrtc: process.platform === 'linux'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// WebTorrent-to-HTTP streaming sever
|
// WebTorrent-to-HTTP streaming sever
|
||||||
var server = window.server = null
|
var server = window.server = null
|
||||||
@@ -42,8 +51,8 @@ function init () {
|
|||||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||||
|
|
||||||
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) =>
|
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
||||||
startTorrenting(torrentKey, torrentID, path, fileModtimes))
|
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
||||||
ipc.on('wt-stop-torrenting', (e, infoHash) =>
|
ipc.on('wt-stop-torrenting', (e, infoHash) =>
|
||||||
stopTorrenting(infoHash))
|
stopTorrenting(infoHash))
|
||||||
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
|
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
|
||||||
@@ -58,6 +67,8 @@ function init () {
|
|||||||
startServer(infoHash, index))
|
startServer(infoHash, index))
|
||||||
ipc.on('wt-stop-server', (e) =>
|
ipc.on('wt-stop-server', (e) =>
|
||||||
stopServer())
|
stopServer())
|
||||||
|
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
||||||
|
selectFiles(infoHash, selections))
|
||||||
|
|
||||||
ipc.send('ipcReadyWebTorrent')
|
ipc.send('ipcReadyWebTorrent')
|
||||||
|
|
||||||
@@ -66,31 +77,27 @@ function init () {
|
|||||||
|
|
||||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
||||||
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||||
function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
|
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
||||||
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
||||||
var torrent
|
|
||||||
try {
|
var torrent = client.add(torrentID, {
|
||||||
torrent = client.add(torrentID, {
|
path: path,
|
||||||
path: path,
|
fileModtimes: fileModtimes
|
||||||
fileModtimes: fileModtimes
|
})
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
return ipc.send('wt-error', torrentKey, err.message)
|
|
||||||
}
|
|
||||||
// If we add a duplicate magnet URI or infohash, WebTorrent returns the
|
|
||||||
// existing torrent object! (If we add a duplicate torrent file, it creates a
|
|
||||||
// new torrent object and raises an error later.) Workaround:
|
|
||||||
if (torrent.key) {
|
|
||||||
return ipc.send('wt-error', torrentKey, 'Can\'t add duplicate torrent')
|
|
||||||
}
|
|
||||||
torrent.key = torrentKey
|
torrent.key = torrentKey
|
||||||
|
|
||||||
|
// Listen for ready event, progress notifications, etc
|
||||||
addTorrentEvents(torrent)
|
addTorrentEvents(torrent)
|
||||||
|
|
||||||
|
// Only download the files the user wants, not necessarily all files
|
||||||
|
torrent.once('ready', () => selectFiles(torrent, selections))
|
||||||
|
|
||||||
return torrent
|
return torrent
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopTorrenting (infoHash) {
|
function stopTorrenting (infoHash) {
|
||||||
var torrent = client.get(infoHash)
|
var torrent = client.get(infoHash)
|
||||||
torrent.destroy()
|
if (torrent) torrent.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new torrent, start seeding
|
// Create a new torrent, start seeding
|
||||||
@@ -159,9 +166,7 @@ function getTorrentFileInfo (file) {
|
|||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
length: file.length,
|
length: file.length,
|
||||||
path: file.path,
|
path: file.path
|
||||||
numPiecesPresent: 0,
|
|
||||||
numPieces: null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +317,44 @@ function getAudioMetadata (infoHash, index) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectFiles (torrentOrInfoHash, selections) {
|
||||||
|
// Get the torrent object
|
||||||
|
var torrent
|
||||||
|
if (typeof torrentOrInfoHash === 'string') {
|
||||||
|
torrent = client.get(torrentOrInfoHash)
|
||||||
|
} else {
|
||||||
|
torrent = torrentOrInfoHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selections not specified?
|
||||||
|
// Load all files. We still need to replace the default whole-torrent
|
||||||
|
// selection with individual selections for each file, so we can
|
||||||
|
// select/deselect files later on
|
||||||
|
if (!selections) {
|
||||||
|
selections = torrent.files.map((x) => true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selections specified incorrectly?
|
||||||
|
if (selections.length !== torrent.files.length) {
|
||||||
|
throw new Error('got ' + selections.length + ' file selections, ' +
|
||||||
|
'but the torrent contains ' + torrent.files.length + ' files')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove default selection (whole torrent)
|
||||||
|
torrent.deselect(0, torrent.pieces.length - 1, false)
|
||||||
|
|
||||||
|
// Add selections (individual files)
|
||||||
|
for (var i = 0; i < selections.length; i++) {
|
||||||
|
var file = torrent.files[i]
|
||||||
|
if (selections[i]) {
|
||||||
|
file.select()
|
||||||
|
} else {
|
||||||
|
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
|
||||||
|
file.deselect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Gets a WebTorrent handle by torrentKey
|
// Gets a WebTorrent handle by torrentKey
|
||||||
// Throws an Error if we're not currently torrenting anything w/ that key
|
// Throws an Error if we're not currently torrenting anything w/ that key
|
||||||
function getTorrent (torrentKey) {
|
function getTorrent (torrentKey) {
|
||||||
|
|||||||
Reference in New Issue
Block a user