Compare commits
247 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09724dddd9 | ||
|
|
59286ff3cb | ||
|
|
3f79c90868 | ||
|
|
33c48d4dfb | ||
|
|
48fbcd7303 | ||
|
|
cdb7b6eb44 | ||
|
|
90b72523b7 | ||
|
|
a1fd30f4f5 | ||
|
|
fcae064dbb | ||
|
|
5815d8efe7 | ||
|
|
0be8a73621 | ||
|
|
76e1d7777c | ||
|
|
e10a84b294 | ||
|
|
5ff2d893b9 | ||
|
|
02f5dbb63f | ||
|
|
d4cfc32c8d | ||
|
|
d5820063a1 | ||
|
|
21de048738 | ||
|
|
06cdac8121 | ||
|
|
f1aa1bdd59 | ||
|
|
44f621b4de | ||
|
|
fd1a1f0f7e | ||
|
|
a38b5220ac | ||
|
|
428a07101a | ||
|
|
f1ef9daa8f | ||
|
|
46d5c9c9e9 | ||
|
|
ecd877551e | ||
|
|
76e071e965 | ||
|
|
5623c1024e | ||
|
|
1a20155f66 | ||
|
|
9a17313a0c | ||
|
|
b3ec61ddd8 | ||
|
|
7dcddf90e9 | ||
|
|
a94fdcae61 | ||
|
|
f9bb83815f | ||
|
|
dccaf16a02 | ||
|
|
f4ee831319 | ||
|
|
5bcf1c6379 | ||
|
|
d2eb87e821 | ||
|
|
be08925eb4 | ||
|
|
5698c22e75 | ||
|
|
2114532f62 | ||
|
|
815ba00a6b | ||
|
|
bcd9f046fb | ||
|
|
bf3b416662 | ||
|
|
736832c4e3 | ||
|
|
f08c0995a2 | ||
|
|
de8a4d1160 | ||
|
|
739c1f705e | ||
|
|
a32889291f | ||
|
|
1e7e4cafd4 | ||
|
|
d5bed6c50a | ||
|
|
8f827c9aae | ||
|
|
24fe033e2f | ||
|
|
85d04f931b | ||
|
|
41b111c8a8 | ||
|
|
72db60bb12 | ||
|
|
076eb009b9 | ||
|
|
789bd0ce82 | ||
|
|
5155fca0e4 | ||
|
|
3b6819f894 | ||
|
|
0dd1683298 | ||
|
|
2788d7433b | ||
|
|
504a2419f6 | ||
|
|
75e5316ba1 | ||
|
|
3be018521a | ||
|
|
6d375d5b5b | ||
|
|
5de39bd7e5 | ||
|
|
a08d576851 | ||
|
|
b8bdf65514 | ||
|
|
1c0c3d07ff | ||
|
|
832980eb9a | ||
|
|
7c158e9f2c | ||
|
|
a98d22ed72 | ||
|
|
63b7d34e29 | ||
|
|
67ae6061aa | ||
|
|
a8a861260e | ||
|
|
fc879d5801 | ||
|
|
2d2645e642 | ||
|
|
8e66f641ce | ||
|
|
82c49b5fc5 | ||
|
|
b57ee73035 | ||
|
|
ed8f493b8b | ||
|
|
9e853027da | ||
|
|
1e05487acd | ||
|
|
167da9dfd5 | ||
|
|
46e138a376 | ||
|
|
853db922f1 | ||
|
|
7bf51b11ee | ||
|
|
3a286ae978 | ||
|
|
82245f0b5c | ||
|
|
802a898394 | ||
|
|
2200fffa1e | ||
|
|
f4b2e78e72 | ||
|
|
ad1162c7de | ||
|
|
ed4daeb560 | ||
|
|
927ae16e4f | ||
|
|
917c89542b | ||
|
|
39570bd4d7 | ||
|
|
f368dfad81 | ||
|
|
205e17cc83 | ||
|
|
4e052c8059 | ||
|
|
0afecd6063 | ||
|
|
41511c5615 | ||
|
|
b9d39e3c64 | ||
|
|
c1f482a950 | ||
|
|
e424031ad9 | ||
|
|
f43dc2fc98 | ||
|
|
9cdc73edce | ||
|
|
3d254fa075 | ||
|
|
ed1e43015e | ||
|
|
e6cbbd73f0 | ||
|
|
67dff7b38c | ||
|
|
ced67176a3 | ||
|
|
00ac8afe64 | ||
|
|
a6964c4495 | ||
|
|
aedbc3c32f | ||
|
|
6541291e0d | ||
|
|
711d274398 | ||
|
|
6c5861b9fc | ||
|
|
f7ab27f9fd | ||
|
|
e4e789cc5b | ||
|
|
09b525fe58 | ||
|
|
9dabfc1367 | ||
|
|
bdb733352a | ||
|
|
75a4655a0f | ||
|
|
051c1516a0 | ||
|
|
62c5b78358 | ||
|
|
290913d07a | ||
|
|
77534d650a | ||
|
|
a4c715e3f6 | ||
|
|
7415d3cee5 | ||
|
|
e1ba9c89fe | ||
|
|
0fcbe7369a | ||
|
|
c8087b5b63 | ||
|
|
065faca8eb | ||
|
|
bcd6a38a05 | ||
|
|
fa67f9b82b | ||
|
|
39acd0bd47 | ||
|
|
c549fcfc27 | ||
|
|
45e838d4c3 | ||
|
|
64f49e4d4f | ||
|
|
61caa90901 | ||
|
|
3e85289318 | ||
|
|
a629f287f0 | ||
|
|
3a4906079b | ||
|
|
3edf21f457 | ||
|
|
785c44cd2a | ||
|
|
1ad8a5313b | ||
|
|
967e161288 | ||
|
|
fe8c3b190c | ||
|
|
993e7d77ad | ||
|
|
e0be052df4 | ||
|
|
d331bae548 | ||
|
|
d88229694a | ||
|
|
8da5b955d6 | ||
|
|
54882679c1 | ||
|
|
f2007be1b0 | ||
|
|
7a757f9e05 | ||
|
|
678f961110 | ||
|
|
f0464c44fd | ||
|
|
b323ee24c4 | ||
|
|
38aad2ee9c | ||
|
|
b69ca93d20 | ||
|
|
e21a039e70 | ||
|
|
11f8e428a0 | ||
|
|
704455c432 | ||
|
|
c25bee755c | ||
|
|
373d598c29 | ||
|
|
b8effffa96 | ||
|
|
ca6a7917ce | ||
|
|
033bdf7908 | ||
|
|
6fe03aa325 | ||
|
|
b93f41f564 | ||
|
|
3f6cc97a02 | ||
|
|
0bda5358bd | ||
|
|
e0af554caa | ||
|
|
b98f8476f5 | ||
|
|
30732305ff | ||
|
|
14102ab3e6 | ||
|
|
df16b14586 | ||
|
|
9dcbc1b1f6 | ||
|
|
d806fd502f | ||
|
|
db650caf18 | ||
|
|
2b6c9ffcdb | ||
|
|
1d4b8ab67d | ||
|
|
6404168bee | ||
|
|
6613366cff | ||
|
|
74349129f4 | ||
|
|
cdc2c1d718 | ||
|
|
f8cc155650 | ||
|
|
b6bdeab50b | ||
|
|
f528f6033f | ||
|
|
ef1bc13c38 | ||
|
|
2f54feac74 | ||
|
|
75d30baaa5 | ||
|
|
990fb57839 | ||
|
|
1883341ddb | ||
|
|
5f7cece6d1 | ||
|
|
89e77d34f4 | ||
|
|
5eeb8fd6fc | ||
|
|
808fca031a | ||
|
|
6b3c1e3802 | ||
|
|
f488ef7597 | ||
|
|
2c179c7465 | ||
|
|
f1cf37200e | ||
|
|
d2da6881d6 | ||
|
|
9c25de58de | ||
|
|
f9aeb676b4 | ||
|
|
396d769bc8 | ||
|
|
83901eecba | ||
|
|
019728cff5 | ||
|
|
a96241d151 | ||
|
|
ebc9771be5 | ||
|
|
0c75bac364 | ||
|
|
3f7e2c1e4a | ||
|
|
10bf38bdf0 | ||
|
|
02508d7d9e | ||
|
|
5cb295f722 | ||
|
|
b08d273996 | ||
|
|
1e27d1803a | ||
|
|
9bc018cc02 | ||
|
|
73cdfc6d45 | ||
|
|
1afd650997 | ||
|
|
9c8eabb46c | ||
|
|
b39884e68f | ||
|
|
451d457426 | ||
|
|
82853aa017 | ||
|
|
157226f75b | ||
|
|
509691a85a | ||
|
|
8b3aee7e2d | ||
|
|
4025e669eb | ||
|
|
1a01d7ed92 | ||
|
|
b4976d27f2 | ||
|
|
173d8444d7 | ||
|
|
aa150b76a5 | ||
|
|
e2b5e28e07 | ||
|
|
1ad07d9977 | ||
|
|
8ba4056894 | ||
|
|
9ad0316dff | ||
|
|
854aae7dc5 | ||
|
|
5b021cd42e | ||
|
|
33417d9b7e | ||
|
|
275184214a | ||
|
|
1f9adbd3cf | ||
|
|
092c207dce | ||
|
|
603c24faed |
7
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
npm-debug.log*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'node'
|
||||
install: npm install standard
|
||||
install: npm install standard depcheck walk-sync
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
- Romain Beaumont (romain.rom1@gmail.com)
|
||||
- Dan Flettre (fletd01@yahoo.com)
|
||||
- Liam Gray (liam.r.gray@gmail.com)
|
||||
- grunjol (grunjol@users.noreply.github.com)
|
||||
- Rémi Jouannet (remijouannet@users.noreply.github.com)
|
||||
- Evan Miller (miller.evan815@gmail.com)
|
||||
- Alex (alxmorais8@msn.com)
|
||||
@@ -28,5 +27,12 @@
|
||||
- Adam Gotlib (gotlib.adam+dev@gmail.com)
|
||||
- Rémi Jouannet (remijouannet@gmail.com)
|
||||
- Andrea Tupini (tupini07@gmail.com)
|
||||
- grunjol (grunjol@gmail.com)
|
||||
- Jason Kurian (jasonk92@gmail.com)
|
||||
- Vamsi Krishna Avula (vamsi_ism@outlook.com)
|
||||
- Noam Okman (noamokman@gmail.com)
|
||||
- PurgingPanda (t3ch0wn3r@gmail.com)
|
||||
- Kai Curtis (morecode@kcurtis.com)
|
||||
- Omri Litov (omrilitov@gmail.com)
|
||||
|
||||
#### Generated by bin/update-authors.sh.
|
||||
|
||||
114
CHANGELOG.md
@@ -1,9 +1,121 @@
|
||||
# WebTorrent Desktop Version History
|
||||
|
||||
## v0.17.1 - 2016-10-03
|
||||
|
||||
### Changed
|
||||
- Faster startup (improved by ~25%)
|
||||
- Update Electron to 1.4.2
|
||||
- Remove support for pasting multiple newline-separated magnet links
|
||||
- Reduce UX sound volume
|
||||
|
||||
### Fixed
|
||||
- Fix external player (VLC, etc.) opening before HTTP server was ready
|
||||
- Windows (Portable App): Fix "Portable App" mode
|
||||
- Write application support files to the "Portable Settings" folder
|
||||
- Stop writing Electron "single instance" lock file to "%APPDATA%\Roaming\WebTorrent"
|
||||
- Some temp data is still written to "%APPDATA%\Temp" (will be fixed in future version)
|
||||
- Don't show pointer cursor on torrent list checkbox
|
||||
- Trim extra whitespace from magnet links pasted into "Open Torrent Address" dialog
|
||||
- Fix weird outline on 'Create Torrent' button
|
||||
|
||||
## v0.17.0 - 2016-09-23
|
||||
|
||||
### Added
|
||||
- Remember window size and position
|
||||
|
||||
### Changed
|
||||
- Torrent list redesign
|
||||
- Quieter, more subtle sounds
|
||||
- Got rid of the play button spinner, now goes to the player immediately
|
||||
- Faster startup
|
||||
|
||||
### Fixed
|
||||
- Fix bug where playback rate could go negative
|
||||
- Don't hide header when moused over player controls
|
||||
- Fix Delete Data File on Windows
|
||||
- Fix a sad, sad bug that resulted in 100+ MB config files
|
||||
- Fix app DMG background image
|
||||
|
||||
## v0.16.0 - 2016-09-18
|
||||
|
||||
### Added
|
||||
- **Windows 64-bit support!** ([#931](https://github.com/feross/webtorrent-desktop/pull/931))
|
||||
- Existing 32-bit users will update to 64-bit automatically in next release
|
||||
- 64-bit reduces likelihood of out-of-memory errors by increasing the address space
|
||||
|
||||
### Fixed
|
||||
- Mac: Fix background image on .DMG
|
||||
|
||||
## v0.15.0 - 2016-09-16
|
||||
|
||||
### Added
|
||||
- Option to start automatically on login
|
||||
- Add integration tests
|
||||
- Add more detailed telemetry to diagnose "buffer allocation failed"
|
||||
|
||||
### Changed
|
||||
- Disable playback controls while in external player (#909)
|
||||
|
||||
### Fixed
|
||||
- Fix several uncaught errors (#889, #891, #892)
|
||||
- Update to the latest webtorrent.js, fixing some more uncaught errors
|
||||
- Clicking on the "torrent finished" notification works again (#912)
|
||||
|
||||
## v0.14.0 - 2016-09-03
|
||||
|
||||
### Added
|
||||
- Autoplay through all files in a torrent (#871)
|
||||
- Torrents now have a progress bar (#844)
|
||||
|
||||
### Changed
|
||||
- Modals now use Material UI
|
||||
- Torrent list style improvements
|
||||
|
||||
### Fixed
|
||||
- Fix App.js crash in Linux (#882)
|
||||
- Fix error on Windows caused by `setBadge` (#867)
|
||||
- Don't crash when restarting after adding a magnet link (#869)
|
||||
- Restore playback state when reopening player (#877)
|
||||
|
||||
## v0.13.1 - 2016-08-31
|
||||
|
||||
### Fixed
|
||||
- Fixed the Create Torrent page
|
||||
|
||||
## v0.13.0 - 2016-08-31
|
||||
|
||||
### Added
|
||||
- Support .m4a audio
|
||||
- Better telemetry: log error versions, report more types of errors
|
||||
|
||||
### Changed
|
||||
- New look - Material UI. Rewrote Create Torrent and Preferences pages.
|
||||
|
||||
### Fixed
|
||||
- Fixed telemetry [object Object] and [object HTMLMediaElement] bugs
|
||||
- Don't render player controls when playing externally, eg in VLC
|
||||
- Don't play notification sounds during media playback
|
||||
|
||||
## v0.12.0 - 2016-08-23
|
||||
|
||||
### Added
|
||||
|
||||
- Custom external media player
|
||||
- Linux: add system-wide launcher and icons for Debian, including Ubuntu
|
||||
|
||||
### Changed
|
||||
|
||||
- Telemetry improvements: redact stacktraces, log app version
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix playback and download of default torrents ("missing path" error) (#804)
|
||||
- Fix Delete Torrent + Data for newly added magnet links
|
||||
- Fix jumpToTime error (#804)
|
||||
|
||||
## v0.11.0 - 2016-08-19
|
||||
|
||||
### Added
|
||||
|
||||
- New Preference to "Set WebTorrent as default handler for torrents and magnet links" (#771)
|
||||
- New Preference to "Always play in VLC" (#674)
|
||||
- Check for missing default download path and torrent folders on start up (#776)
|
||||
|
||||
84
README.md
@@ -7,7 +7,7 @@
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h4 align="center">The streaming torrent client. For Mac, Windows, and Linux.</h4>
|
||||
<h4 align="center">The streaming torrent app. For Mac, Windows, and Linux.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitter.im/feross/webtorrent"><img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg" alt="Gitter"></a>
|
||||
@@ -17,29 +17,72 @@
|
||||
|
||||
## Install
|
||||
|
||||
**WebTorrent Desktop** is still under very active development. You can download the latest version from the [releases](https://github.com/feross/webtorrent-desktop/releases) page.
|
||||
Download the latest version of WebTorrent Desktop from
|
||||
[the official website](https://webtorrent.io/desktop/) or the
|
||||
[GitHub releases](https://github.com/feross/webtorrent-desktop/releases) page.
|
||||
|
||||
## Screenshot
|
||||
**WebTorrent Desktop** is under very active development. You can try out the
|
||||
current (unstable) development version by cloning the Git repo. See the
|
||||
instructions below in the ["How to Contribute"](#how-to-contribute) section.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p align="center">
|
||||
<img src="https://webtorrent.io/img/screenshot-player3.png" alt="screenshot" align="center">
|
||||
<img src="https://webtorrent.io/img/screenshot-main.png" width="612" height="749" alt="screenshot" align="center">
|
||||
</p>
|
||||
|
||||
## How to Contribute
|
||||
|
||||
### Install dependencies
|
||||
### Get the code
|
||||
|
||||
```
|
||||
$ git clone https://github.com/feross/webtorrent-desktop.git
|
||||
$ cd webtorrent-desktop
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### Run app
|
||||
### Run the app
|
||||
|
||||
```
|
||||
$ npm start
|
||||
```
|
||||
|
||||
### Package app
|
||||
### Watch the code
|
||||
|
||||
Restart the app automatically every time code changes. Useful during development.
|
||||
|
||||
```
|
||||
$ npm run watch
|
||||
```
|
||||
|
||||
### Run linters
|
||||
|
||||
```
|
||||
$ npm test
|
||||
```
|
||||
|
||||
### Run integration tests
|
||||
|
||||
```
|
||||
$ npm run test-integration
|
||||
```
|
||||
|
||||
The integration tests use Spectron and Tape. They click through the app, taking screenshots and comparing each one to a reference. Why screenshots?
|
||||
|
||||
* Ad-hoc checking makes the tests a lot more work to write
|
||||
* Even diffing the whole HTML is not as thorough as screenshot diffing. For example, it wouldn't catch an bug where hitting ESC from a video doesn't correctly restore window size.
|
||||
* Chrome's own integration tests use screenshot diffing iirc
|
||||
* Small UI changes will break a few tests, but the fix is as easy as deleting the offending screenshots and running the tests, which will recreate them with the new look.
|
||||
* The resulting Github PR will then show, pixel by pixel, the exact UI changes that were made! Ses https://github.com/blog/817-behold-image-view-modes
|
||||
|
||||
For MacOS, you'll need a Retina screen for the integration tests to pass. Your screen should have the same resolution as a 2016 12" Macbook.
|
||||
|
||||
For Windows, you'll need Windows 10 with a 1366x768 screen.
|
||||
|
||||
When running integration tests, keep the mouse on the edge of the screen and don't touch the mouse or keyboard while the tests are running.
|
||||
|
||||
### Package the app
|
||||
|
||||
Builds app binaries for Mac, Linux, and Windows.
|
||||
|
||||
@@ -50,7 +93,7 @@ $ npm run package
|
||||
To build for one platform:
|
||||
|
||||
```
|
||||
$ npm run package -- [platform]
|
||||
$ npm run package -- [platform] [options]
|
||||
```
|
||||
|
||||
Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default).
|
||||
@@ -66,14 +109,18 @@ The following optional arguments are available:
|
||||
- `portable` - Windows portable app
|
||||
- `all` - All platforms (default)
|
||||
|
||||
Note: Even with the `--package` option, the auto-update files (.nupkg for Windows, *-darwin.zip for Mac) will always be produced.
|
||||
Note: Even with the `--package` option, the auto-update files (.nupkg for Windows,
|
||||
*-darwin.zip for Mac) will always be produced.
|
||||
|
||||
#### Windows build notes
|
||||
|
||||
To package the Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs
|
||||
to be installed.
|
||||
The Windows app can be packaged from **any** platform.
|
||||
|
||||
On Mac, first install [XQuartz](http://www.xquartz.org/), then run:
|
||||
Note: Windows code signing only works from **Windows**, for now.
|
||||
|
||||
Note: To package the Windows app from non-Windows platforms,
|
||||
[Wine](https://www.winehq.org/) needs to be installed. For example on Mac, first
|
||||
install [XQuartz](http://www.xquartz.org/), then run:
|
||||
|
||||
```
|
||||
brew install wine
|
||||
@@ -81,11 +128,22 @@ brew install wine
|
||||
|
||||
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
||||
|
||||
#### Mac build notes
|
||||
|
||||
The Mac app can only be packaged from **macOS**.
|
||||
|
||||
#### Linux build notes
|
||||
|
||||
The Linux app can be packaged from **any** platform.
|
||||
|
||||
### Privacy
|
||||
|
||||
WebTorrent Desktop collects some basic usage stats to help us make the app better. For example, we track how well the play button works. How often does it succeed? Time out? Show a missing codec error?
|
||||
WebTorrent Desktop collects some basic usage stats to help us make the app better.
|
||||
For example, we track how well the play button works. How often does it succeed?
|
||||
Time out? Show a missing codec error?
|
||||
|
||||
The app never sends personally identifying information, nor does it track which swarms you join.
|
||||
The app never sends any personally identifying information, nor does it track which
|
||||
torrents you add.
|
||||
|
||||
### Code Style
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var fs = require('fs')
|
||||
var cp = require('child_process')
|
||||
|
||||
// We can't use `builtin-modules` here since our TravisCI
|
||||
// setup expects this file to run with no dependencies
|
||||
var BUILT_IN_NODE_MODULES = [
|
||||
'assert',
|
||||
'buffer',
|
||||
'child_process',
|
||||
'cluster',
|
||||
'console',
|
||||
'constants',
|
||||
'crypto',
|
||||
'dgram',
|
||||
'dns',
|
||||
'domain',
|
||||
'events',
|
||||
'fs',
|
||||
'http',
|
||||
'https',
|
||||
'module',
|
||||
'net',
|
||||
'os',
|
||||
'path',
|
||||
'process',
|
||||
'punycode',
|
||||
'querystring',
|
||||
'readline',
|
||||
'repl',
|
||||
'stream',
|
||||
'string_decoder',
|
||||
'timers',
|
||||
'tls',
|
||||
'tty',
|
||||
'url',
|
||||
'util',
|
||||
'v8',
|
||||
'vm',
|
||||
'zlib'
|
||||
]
|
||||
|
||||
var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
|
||||
|
||||
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
|
||||
|
||||
var EXECUTABLE_DEPS = [
|
||||
'gh-release',
|
||||
'standard',
|
||||
'babel-cli',
|
||||
'babel-plugin-syntax-jsx',
|
||||
'babel-plugin-transform-react-jsx'
|
||||
]
|
||||
|
||||
main()
|
||||
|
||||
// Scans codebase for missing or unused dependencies. Exits with code 0 on success.
|
||||
function main () {
|
||||
if (process.platform === 'win32') {
|
||||
console.error('Sorry, check-deps only works on Mac and Linux')
|
||||
return
|
||||
}
|
||||
|
||||
var usedDeps = findUsedDeps()
|
||||
var packageDeps = findPackageDeps()
|
||||
|
||||
var missingDeps = usedDeps.filter(
|
||||
(dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep)
|
||||
)
|
||||
var unusedDeps = packageDeps.filter(
|
||||
(dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
|
||||
)
|
||||
|
||||
if (missingDeps.length > 0) {
|
||||
console.error('Missing package dependencies: ' + missingDeps)
|
||||
}
|
||||
if (unusedDeps.length > 0) {
|
||||
console.error('Unused package dependencies: ' + unusedDeps)
|
||||
}
|
||||
if (missingDeps.length + unusedDeps.length > 0) {
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Finds all dependencies specified in `package.json`
|
||||
function findPackageDeps () {
|
||||
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||
|
||||
var deps = Object.keys(pkg.dependencies)
|
||||
var devDeps = Object.keys(pkg.devDependencies)
|
||||
var optionalDeps = Object.keys(pkg.optionalDependencies)
|
||||
|
||||
return [].concat(deps, devDeps, optionalDeps)
|
||||
}
|
||||
|
||||
// Finds all dependencies that used with `require()`
|
||||
function findUsedDeps () {
|
||||
var stdout = cp.execSync('./bin/list-deps.sh')
|
||||
return stdout.toString().trim().split('\n')
|
||||
}
|
||||
|
||||
function includes (arr, elem) {
|
||||
return arr.indexOf(elem) >= 0
|
||||
}
|
||||
14
bin/clean.js
@@ -5,13 +5,13 @@
|
||||
* Useful for developers.
|
||||
*/
|
||||
|
||||
var fs = require('fs')
|
||||
var os = require('os')
|
||||
var path = require('path')
|
||||
var rimraf = require('rimraf')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const rimraf = require('rimraf')
|
||||
|
||||
var config = require('../src/config')
|
||||
var handlers = require('../src/main/handlers')
|
||||
const config = require('../src/config')
|
||||
const handlers = require('../src/main/handlers')
|
||||
|
||||
// First, remove generated files
|
||||
rimraf.sync('build/')
|
||||
@@ -21,7 +21,7 @@ rimraf.sync('dist/')
|
||||
rimraf.sync(config.CONFIG_PATH)
|
||||
|
||||
// Remove any temporary files
|
||||
var tmpPath
|
||||
let tmpPath
|
||||
try {
|
||||
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
|
||||
} catch (err) {
|
||||
|
||||
10
bin/cmd.js
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var electron = require('electron-prebuilt')
|
||||
var cp = require('child_process')
|
||||
var path = require('path')
|
||||
|
||||
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
|
||||
child.on('close', function (code) {
|
||||
process.exitCode = code
|
||||
})
|
||||
46
bin/extra-lint.js
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const walkSync = require('walk-sync')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
let hasErrors = false
|
||||
|
||||
// Find all Javascript source files
|
||||
const files = walkSync('src', {globs: ['**/*.js']})
|
||||
console.log('Running extra-lint on ' + files.length + ' files...')
|
||||
|
||||
// Read each file, line by line
|
||||
files.forEach(function (file) {
|
||||
const filepath = path.join('src', file)
|
||||
const lines = fs.readFileSync(filepath, 'utf8').split('\n')
|
||||
|
||||
lines.forEach(function (line, i) {
|
||||
let error
|
||||
|
||||
// Consistent JSX tag closing
|
||||
if (line.match(/' {2}\/> *$/) ||
|
||||
line.match('[^ ]/> *$') ||
|
||||
line.match(' > *$')) {
|
||||
error = 'JSX tag spacing'
|
||||
}
|
||||
|
||||
// No lines over 100 characters
|
||||
if (line.length > 100) {
|
||||
error = 'Line >100 chars'
|
||||
}
|
||||
|
||||
if (line.match(/^var /) || line.match(/ var /)) {
|
||||
error = 'Use const or let'
|
||||
}
|
||||
|
||||
if (error) {
|
||||
let name = path.basename(file)
|
||||
console.log('%s:%d - %s:\n%s', name, i + 1, error, line)
|
||||
hasErrors = true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (hasErrors) process.exit(1)
|
||||
else console.log('Looks good!')
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
# This is a truly heinous hack, but it works pretty nicely.
|
||||
# Find all modules we're requiring---even conditional requires.
|
||||
|
||||
grep "require('" src/ bin/ -R |
|
||||
grep '.js:' |
|
||||
sed "s/.*require('\([^'\/]*\).*/\1/" |
|
||||
grep -v '^\.' |
|
||||
sort |
|
||||
uniq
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var config = require('../src/config')
|
||||
var open = require('open')
|
||||
const config = require('../src/config')
|
||||
const open = require('open')
|
||||
|
||||
open(config.CONFIG_PATH)
|
||||
|
||||
234
bin/package.js
@@ -1,28 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Builds app binaries for Mac, Linux, and Windows.
|
||||
* Builds app binaries for Mac, Windows, and Linux.
|
||||
*/
|
||||
|
||||
var cp = require('child_process')
|
||||
var electronPackager = require('electron-packager')
|
||||
var fs = require('fs')
|
||||
var minimist = require('minimist')
|
||||
var mkdirp = require('mkdirp')
|
||||
var os = require('os')
|
||||
var path = require('path')
|
||||
var rimraf = require('rimraf')
|
||||
var series = require('run-series')
|
||||
var zip = require('cross-zip')
|
||||
const cp = require('child_process')
|
||||
const electronPackager = require('electron-packager')
|
||||
const fs = require('fs')
|
||||
const minimist = require('minimist')
|
||||
const mkdirp = require('mkdirp')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const rimraf = require('rimraf')
|
||||
const series = require('run-series')
|
||||
const zip = require('cross-zip')
|
||||
|
||||
var config = require('../src/config')
|
||||
var pkg = require('../package.json')
|
||||
const config = require('../src/config')
|
||||
const pkg = require('../package.json')
|
||||
|
||||
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
||||
var BUILD_PATH = path.join(config.ROOT_PATH, 'build')
|
||||
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
||||
const BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
||||
const BUILD_PATH = path.join(config.ROOT_PATH, 'build')
|
||||
const DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
||||
|
||||
var argv = minimist(process.argv.slice(2), {
|
||||
const argv = minimist(process.argv.slice(2), {
|
||||
boolean: [
|
||||
'sign'
|
||||
],
|
||||
@@ -39,11 +39,11 @@ function build () {
|
||||
rimraf.sync(DIST_PATH)
|
||||
rimraf.sync(BUILD_PATH)
|
||||
|
||||
console.log('Babel: Building JSX...')
|
||||
console.log('Build: Transpiling to ES5...')
|
||||
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
|
||||
console.log('Babel: Built JSX.')
|
||||
console.log('Build: Transpiled to ES5.')
|
||||
|
||||
var platform = argv._[0]
|
||||
const platform = argv._[0]
|
||||
if (platform === 'darwin') {
|
||||
buildDarwin(printDone)
|
||||
} else if (platform === 'win32') {
|
||||
@@ -61,7 +61,7 @@ function build () {
|
||||
}
|
||||
}
|
||||
|
||||
var all = {
|
||||
const all = {
|
||||
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
|
||||
// property on Windows, and `NSHumanReadableCopyright` on Mac.
|
||||
'app-copyright': config.APP_COPYRIGHT,
|
||||
@@ -73,11 +73,11 @@ var all = {
|
||||
// Package the application's source code into an archive, using Electron's archive
|
||||
// format. Mitigates issues around long path names on Windows and slightly speeds up
|
||||
// require().
|
||||
asar: true,
|
||||
|
||||
// A glob expression, that unpacks the files with matching names to the
|
||||
// "app.asar.unpacked" directory.
|
||||
'asar-unpack': 'WebTorrent*',
|
||||
asar: {
|
||||
// A glob expression, that unpacks the files with matching names to the
|
||||
// "app.asar.unpacked" directory.
|
||||
unpack: 'WebTorrent*'
|
||||
},
|
||||
|
||||
// The build version of the application. Maps to the FileVersion metadata property on
|
||||
// Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to
|
||||
@@ -105,14 +105,14 @@ var all = {
|
||||
prune: true,
|
||||
|
||||
// The Electron version with which the app is built (without the leading 'v')
|
||||
version: require('electron-prebuilt/package.json').version
|
||||
version: require('electron/package.json').version
|
||||
}
|
||||
|
||||
var darwin = {
|
||||
const darwin = {
|
||||
// Build for Mac
|
||||
platform: 'darwin',
|
||||
|
||||
// Build 64 bit binaries only.
|
||||
// Build x64 binaries only.
|
||||
arch: 'x64',
|
||||
|
||||
// The bundle identifier to use in the application's plist (Mac only).
|
||||
@@ -129,15 +129,15 @@ var darwin = {
|
||||
icon: config.APP_ICON + '.icns'
|
||||
}
|
||||
|
||||
var win32 = {
|
||||
const win32 = {
|
||||
// Build for Windows.
|
||||
platform: 'win32',
|
||||
|
||||
// Build 32 bit binaries only.
|
||||
arch: 'ia32',
|
||||
// Build ia32 and x64 binaries.
|
||||
arch: 'all',
|
||||
|
||||
// Object hash of application metadata to embed into the executable (Windows only)
|
||||
'version-string': {
|
||||
win32metadata: {
|
||||
|
||||
// Company that produced the file.
|
||||
CompanyName: config.APP_NAME,
|
||||
@@ -163,11 +163,11 @@ var win32 = {
|
||||
icon: config.APP_ICON + '.ico'
|
||||
}
|
||||
|
||||
var linux = {
|
||||
const linux = {
|
||||
// Build for Linux.
|
||||
platform: 'linux',
|
||||
|
||||
// Build 32 and 64 bit binaries.
|
||||
// Build ia32 and x64 binaries.
|
||||
arch: 'all'
|
||||
|
||||
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
||||
@@ -176,18 +176,18 @@ var linux = {
|
||||
build()
|
||||
|
||||
function buildDarwin (cb) {
|
||||
var plist = require('plist')
|
||||
const plist = require('plist')
|
||||
|
||||
console.log('Mac: Packaging electron...')
|
||||
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
|
||||
if (err) return cb(err)
|
||||
console.log('Mac: Packaged electron. ' + buildPath)
|
||||
|
||||
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||
var contentsPath = path.join(appPath, 'Contents')
|
||||
var resourcesPath = path.join(contentsPath, 'Resources')
|
||||
var infoPlistPath = path.join(contentsPath, 'Info.plist')
|
||||
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
|
||||
const appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||
const contentsPath = path.join(appPath, 'Contents')
|
||||
const resourcesPath = path.join(contentsPath, 'Resources')
|
||||
const infoPlistPath = path.join(contentsPath, 'Info.plist')
|
||||
const infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
|
||||
|
||||
infoPlist.CFBundleDocumentTypes = [
|
||||
{
|
||||
@@ -261,7 +261,7 @@ function buildDarwin (cb) {
|
||||
}
|
||||
|
||||
function signApp (cb) {
|
||||
var sign = require('electron-osx-sign')
|
||||
const sign = require('electron-osx-sign')
|
||||
|
||||
/*
|
||||
* Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
|
||||
@@ -276,7 +276,7 @@ function buildDarwin (cb) {
|
||||
* - Xcode Command Line Tools (xcode-select --install)
|
||||
* - Membership in the Apple Developer Program
|
||||
*/
|
||||
var signOpts = {
|
||||
const signOpts = {
|
||||
app: appPath,
|
||||
platform: 'darwin',
|
||||
verbose: true
|
||||
@@ -302,8 +302,8 @@ function buildDarwin (cb) {
|
||||
// Create .zip file (used by the auto-updater)
|
||||
console.log('Mac: Creating zip...')
|
||||
|
||||
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
|
||||
const inPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||
const outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
|
||||
zip.zipSync(inPath, outPath)
|
||||
|
||||
console.log('Mac: Created zip.')
|
||||
@@ -312,13 +312,13 @@ function buildDarwin (cb) {
|
||||
function packageDmg (cb) {
|
||||
console.log('Mac: Creating dmg...')
|
||||
|
||||
var appDmg = require('appdmg')
|
||||
const appDmg = require('appdmg')
|
||||
|
||||
var targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
|
||||
const targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
|
||||
rimraf.sync(targetPath)
|
||||
|
||||
// Create a .dmg (Mac disk image) file, for easy user installation.
|
||||
var dmgOpts = {
|
||||
const dmgOpts = {
|
||||
basepath: config.ROOT_PATH,
|
||||
target: targetPath,
|
||||
specification: {
|
||||
@@ -339,7 +339,7 @@ function buildDarwin (cb) {
|
||||
}
|
||||
}
|
||||
|
||||
var dmg = appDmg(dmgOpts)
|
||||
const dmg = appDmg(dmgOpts)
|
||||
dmg.once('error', cb)
|
||||
dmg.on('progress', function (info) {
|
||||
if (info.type === 'step-begin') console.log(info.title + '...')
|
||||
@@ -353,7 +353,7 @@ function buildDarwin (cb) {
|
||||
}
|
||||
|
||||
function buildWin32 (cb) {
|
||||
var installer = require('electron-winstaller')
|
||||
const installer = require('electron-winstaller')
|
||||
console.log('Windows: Packaging electron...')
|
||||
|
||||
/*
|
||||
@@ -361,7 +361,7 @@ function buildWin32 (cb) {
|
||||
* - Windows Authenticode private key and cert (authenticode.p12)
|
||||
* - Windows Authenticode password file (authenticode.txt)
|
||||
*/
|
||||
var CERT_PATH
|
||||
let CERT_PATH
|
||||
try {
|
||||
fs.accessSync('D:')
|
||||
CERT_PATH = 'D:'
|
||||
@@ -373,12 +373,12 @@ function buildWin32 (cb) {
|
||||
if (err) return cb(err)
|
||||
console.log('Windows: Packaged electron. ' + buildPath)
|
||||
|
||||
var signWithParams
|
||||
let signWithParams
|
||||
if (process.platform === 'win32') {
|
||||
if (argv.sign) {
|
||||
var certificateFile = path.join(CERT_PATH, 'authenticode.p12')
|
||||
var certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
|
||||
var timestampServer = 'http://timestamp.comodoca.com'
|
||||
const certificateFile = path.join(CERT_PATH, 'authenticode.p12')
|
||||
const certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
|
||||
const timestampServer = 'http://timestamp.comodoca.com'
|
||||
signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256`
|
||||
} else {
|
||||
printWarning()
|
||||
@@ -387,20 +387,26 @@ function buildWin32 (cb) {
|
||||
printWarning()
|
||||
}
|
||||
|
||||
var tasks = []
|
||||
if (argv.package === 'exe' || argv.package === 'all') {
|
||||
tasks.push((cb) => packageInstaller(cb))
|
||||
}
|
||||
if (argv.package === 'portable' || argv.package === 'all') {
|
||||
tasks.push((cb) => packagePortable(cb))
|
||||
}
|
||||
const tasks = []
|
||||
buildPath.forEach(function (filesPath) {
|
||||
const destArch = filesPath.split('-').pop()
|
||||
|
||||
if (argv.package === 'exe' || argv.package === 'all') {
|
||||
tasks.push((cb) => packageInstaller(filesPath, destArch, cb))
|
||||
}
|
||||
if (argv.package === 'portable' || argv.package === 'all') {
|
||||
tasks.push((cb) => packagePortable(filesPath, destArch, cb))
|
||||
}
|
||||
})
|
||||
series(tasks, cb)
|
||||
|
||||
function packageInstaller (cb) {
|
||||
console.log('Windows: Creating installer...')
|
||||
function packageInstaller (filesPath, destArch, cb) {
|
||||
console.log(`Windows: Creating ${destArch} installer...`)
|
||||
|
||||
const archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||
|
||||
installer.createWindowsInstaller({
|
||||
appDirectory: buildPath[0],
|
||||
appDirectory: filesPath,
|
||||
authors: config.APP_TEAM,
|
||||
description: config.APP_NAME,
|
||||
exe: config.APP_NAME + '.exe',
|
||||
@@ -410,8 +416,26 @@ function buildWin32 (cb) {
|
||||
noMsi: true,
|
||||
outputDirectory: DIST_PATH,
|
||||
productName: config.APP_NAME,
|
||||
remoteReleases: config.GITHUB_URL,
|
||||
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
||||
/**
|
||||
* Only create delta updates for the Windows x64 build because 90% of our
|
||||
* users have Windows x64 and the delta files take a *very* long time to
|
||||
* generate. Also, the ia32 files on GitHub have non-standard Squirrel
|
||||
* names (i.e. RELEASES-ia32 instead of RELEASES) and so Squirrel won't
|
||||
* find them unless we proxy the requests.
|
||||
*/
|
||||
// TODO: Re-enable Windows 64-bit delta updates when we confirm that they
|
||||
// work correctly in the presence of the "ia32" .nupkg files. I
|
||||
// (feross) noticed them listed in the 64-bit RELEASES file and
|
||||
// manually edited them out for the v0.17 release. Shipping only
|
||||
// full updates for now will work fine, with no ill-effects.
|
||||
// remoteReleases: destArch === 'x64'
|
||||
// ? config.GITHUB_URL
|
||||
// : undefined,
|
||||
/**
|
||||
* If you hit a "GitHub API rate limit exceeded" error, set this token!
|
||||
*/
|
||||
// remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
|
||||
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe',
|
||||
setupIcon: config.APP_ICON + '.ico',
|
||||
signWithParams: signWithParams,
|
||||
title: config.APP_NAME,
|
||||
@@ -419,23 +443,71 @@ function buildWin32 (cb) {
|
||||
version: pkg.version
|
||||
})
|
||||
.then(function () {
|
||||
console.log('Windows: Created installer.')
|
||||
console.log(`Windows: Created ${destArch} installer.`)
|
||||
|
||||
/**
|
||||
* Delete extraneous Squirrel files (i.e. *.nupkg delta files for older
|
||||
* versions of the app)
|
||||
*/
|
||||
fs.readdirSync(DIST_PATH)
|
||||
.filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version))
|
||||
.forEach((filename) => {
|
||||
fs.unlinkSync(path.join(DIST_PATH, filename))
|
||||
})
|
||||
|
||||
if (destArch === 'ia32') {
|
||||
console.log('Windows: Renaming ia32 installer files...')
|
||||
|
||||
// RELEASES -> RELEASES-ia32
|
||||
const relPath = path.join(DIST_PATH, 'RELEASES-ia32')
|
||||
fs.renameSync(
|
||||
path.join(DIST_PATH, 'RELEASES'),
|
||||
relPath
|
||||
)
|
||||
|
||||
// WebTorrent-vX.X.X-full.nupkg -> WebTorrent-vX.X.X-ia32-full.nupkg
|
||||
fs.renameSync(
|
||||
path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-full.nupkg`),
|
||||
path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-ia32-full.nupkg`)
|
||||
)
|
||||
|
||||
// Change file name inside RELEASES-ia32 to match renamed file
|
||||
const relContent = fs.readFileSync(relPath, 'utf8')
|
||||
const relContent32 = relContent.replace('full.nupkg', 'ia32-full.nupkg')
|
||||
fs.writeFileSync(relPath, relContent32)
|
||||
|
||||
if (relContent === relContent32) {
|
||||
// Sanity check
|
||||
throw new Error('Fixing RELEASES-ia32 failed. Replacement did not modify the file.')
|
||||
}
|
||||
|
||||
console.log('Windows: Renamed ia32 installer files.')
|
||||
}
|
||||
|
||||
cb(null)
|
||||
})
|
||||
.catch(cb)
|
||||
}
|
||||
|
||||
function packagePortable (cb) {
|
||||
console.log('Windows: Creating portable app...')
|
||||
function packagePortable (filesPath, destArch, cb) {
|
||||
console.log(`Windows: Creating ${destArch} portable app...`)
|
||||
|
||||
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
||||
const portablePath = path.join(filesPath, 'Portable Settings')
|
||||
mkdirp.sync(portablePath)
|
||||
|
||||
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
||||
const downloadsPath = path.join(portablePath, 'Downloads')
|
||||
mkdirp.sync(downloadsPath)
|
||||
|
||||
const tempPath = path.join(portablePath, 'Temp')
|
||||
mkdirp.sync(tempPath)
|
||||
|
||||
const archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||
|
||||
const inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||
const outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip')
|
||||
zip.zipSync(inPath, outPath)
|
||||
|
||||
console.log('Windows: Created portable app.')
|
||||
console.log(`Windows: Created ${destArch} portable app.`)
|
||||
cb(null)
|
||||
}
|
||||
})
|
||||
@@ -447,9 +519,9 @@ function buildLinux (cb) {
|
||||
if (err) return cb(err)
|
||||
console.log('Linux: Packaged electron. ' + buildPath)
|
||||
|
||||
var tasks = []
|
||||
const tasks = []
|
||||
buildPath.forEach(function (filesPath) {
|
||||
var destArch = filesPath.split('-').pop()
|
||||
const destArch = filesPath.split('-').pop()
|
||||
|
||||
if (argv.package === 'deb' || argv.package === 'all') {
|
||||
tasks.push((cb) => packageDeb(filesPath, destArch, cb))
|
||||
@@ -465,8 +537,8 @@ function buildLinux (cb) {
|
||||
// Create .deb file for Debian-based platforms
|
||||
console.log(`Linux: Creating ${destArch} deb...`)
|
||||
|
||||
var deb = require('nobin-debian-installer')()
|
||||
var destPath = path.join('/opt', pkg.name)
|
||||
const deb = require('nobin-debian-installer')()
|
||||
const destPath = path.join('/opt', pkg.name)
|
||||
|
||||
deb.pack({
|
||||
package: pkg,
|
||||
@@ -500,8 +572,10 @@ function buildLinux (cb) {
|
||||
// Create .zip file for Linux
|
||||
console.log(`Linux: Creating ${destArch} zip...`)
|
||||
|
||||
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
|
||||
const archStr = destArch === 'ia32' ? '-ia32' : ''
|
||||
|
||||
const inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||
const outPath = path.join(DIST_PATH, BUILD_NAME + '-linux' + archStr + '.zip')
|
||||
zip.zipSync(inPath, outPath)
|
||||
|
||||
console.log(`Linux: Created ${destArch} zip.`)
|
||||
|
||||
@@ -6,5 +6,4 @@ git diff --exit-code
|
||||
npm run package -- --sign
|
||||
git push
|
||||
git push --tags
|
||||
npm publish
|
||||
./node_modules/.bin/gh-release
|
||||
npm run gh-release
|
||||
|
||||
@@ -12,6 +12,7 @@ while (<>) {
|
||||
next if /(ungoldman\@gmail.com)/;
|
||||
next if /(dc\@DCs-MacBook.local)/;
|
||||
next if /(rolandoguedes\@gmail.com)/;
|
||||
next if /(grunjol\@users.noreply.github.com)/;
|
||||
$seen{$_} = push @authors, "- ", $_;
|
||||
}
|
||||
END {
|
||||
|
||||
46
package.json
@@ -1,39 +1,45 @@
|
||||
{
|
||||
"name": "webtorrent-desktop",
|
||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||
"version": "0.12.0",
|
||||
"version": "0.17.1",
|
||||
"author": {
|
||||
"name": "WebTorrent, LLC",
|
||||
"email": "feross@webtorrent.io",
|
||||
"url": "https://webtorrent.io"
|
||||
},
|
||||
"bin": {
|
||||
"webtorrent-desktop": "./bin/cmd.js"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/feross/webtorrent-desktop/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"airplayer": "^2.0.0",
|
||||
"application-config": "^1.0.0",
|
||||
"arch": "^2.0.0",
|
||||
"auto-launch": "^4.0.1",
|
||||
"bitfield": "^1.0.2",
|
||||
"capture-frame": "^1.0.0",
|
||||
"chromecasts": "^1.8.0",
|
||||
"cp-file": "^4.0.1",
|
||||
"create-torrent": "^3.24.5",
|
||||
"debounce": "^1.0.0",
|
||||
"deep-equal": "^1.0.1",
|
||||
"dlnacasts": "^0.1.0",
|
||||
"drag-drop": "^2.12.1",
|
||||
"electron-prebuilt": "1.3.3",
|
||||
"fs-extra": "^0.30.0",
|
||||
"hat": "0.0.3",
|
||||
"es6-error": "^3.0.1",
|
||||
"fn-getter": "^1.0.0",
|
||||
"iso-639-1": "^1.2.1",
|
||||
"languagedetect": "^1.1.1",
|
||||
"location-history": "^1.0.0",
|
||||
"lodash.merge": "^4.6.0",
|
||||
"material-ui": "^0.15.4",
|
||||
"mkdirp": "^0.5.1",
|
||||
"musicmetadata": "^2.0.2",
|
||||
"network-address": "^1.1.0",
|
||||
"parse-torrent": "^5.7.3",
|
||||
"prettier-bytes": "^1.0.1",
|
||||
"react": "^15.2.1",
|
||||
"react-dom": "^15.2.1",
|
||||
"react-tap-event-plugin": "^1.0.0",
|
||||
"rimraf": "^2.5.2",
|
||||
"run-parallel": "^1.1.6",
|
||||
"semver": "^5.1.0",
|
||||
"simple-concat": "^1.0.0",
|
||||
@@ -45,22 +51,25 @@
|
||||
"zero-fill": "^2.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.11.4",
|
||||
"babel-plugin-syntax-jsx": "^6.13.0",
|
||||
"babel-plugin-transform-react-jsx": "^6.8.0",
|
||||
"buble": "^0.14.0",
|
||||
"cross-zip": "^2.0.1",
|
||||
"depcheck": "^0.6.4",
|
||||
"electron": "1.4.2",
|
||||
"electron-osx-sign": "^0.3.0",
|
||||
"electron-packager": "^7.0.0",
|
||||
"electron-packager": "^8.0.0",
|
||||
"electron-winstaller": "^2.3.0",
|
||||
"gh-release": "^2.0.3",
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"nobin-debian-installer": "^0.0.10",
|
||||
"nodemon": "^1.10.2",
|
||||
"open": "0.0.5",
|
||||
"plist": "^2.0.1",
|
||||
"rimraf": "^2.5.2",
|
||||
"pngjs": "^3.0.0",
|
||||
"run-series": "^1.1.4",
|
||||
"standard": "^8.0.0-beta.5"
|
||||
"spectron": "^3.3.0",
|
||||
"standard": "*",
|
||||
"tape": "^4.6.0",
|
||||
"walk-sync": "^0.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
@@ -87,13 +96,16 @@
|
||||
"url": "git://github.com/feross/webtorrent-desktop.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel --quiet src --out-dir build",
|
||||
"build": "buble src --output build",
|
||||
"clean": "node ./bin/clean.js",
|
||||
"gh-release": "gh-release",
|
||||
"open-config": "node ./bin/open-config.js",
|
||||
"package": "node ./bin/package.js",
|
||||
"prepublish": "npm run build",
|
||||
"start": "npm run build && electron .",
|
||||
"test": "standard && node ./bin/check-deps.js",
|
||||
"update-authors": "./bin/update-authors.sh"
|
||||
"test": "standard && depcheck --ignores=buble,lodash.merge,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
|
||||
"test-integration": "npm run build && node ./test",
|
||||
"update-authors": "./bin/update-authors.sh",
|
||||
"watch": "nodemon --exec \"npm run start\" --ext js,pug,css --ignore build/ --ignore dist/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"syntax-jsx",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,21 @@
|
||||
var appConfig = require('application-config')('WebTorrent')
|
||||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
const appConfig = require('application-config')('WebTorrent')
|
||||
const path = require('path')
|
||||
const electron = require('electron')
|
||||
const arch = require('arch')
|
||||
|
||||
var APP_NAME = 'WebTorrent'
|
||||
var APP_TEAM = 'WebTorrent, LLC'
|
||||
var APP_VERSION = require('../package.json').version
|
||||
const APP_NAME = 'WebTorrent'
|
||||
const APP_TEAM = 'WebTorrent, LLC'
|
||||
const APP_VERSION = require('../package.json').version
|
||||
|
||||
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||
const IS_TEST = isTest()
|
||||
const PORTABLE_PATH = IS_TEST
|
||||
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
|
||||
: path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||
const IS_PRODUCTION = isProduction()
|
||||
const IS_PORTABLE = isPortable()
|
||||
|
||||
const UI_HEADER_HEIGHT = 38
|
||||
const UI_TORRENT_HEIGHT = 100
|
||||
|
||||
module.exports = {
|
||||
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||
@@ -26,26 +35,31 @@ module.exports = {
|
||||
|
||||
DEFAULT_TORRENTS: [
|
||||
{
|
||||
testID: 'bbb',
|
||||
name: 'Big Buck Bunny',
|
||||
posterFileName: 'bigBuckBunny.jpg',
|
||||
torrentFileName: 'bigBuckBunny.torrent'
|
||||
},
|
||||
{
|
||||
testID: 'cosmos',
|
||||
name: 'Cosmos Laundromat (Preview)',
|
||||
posterFileName: 'cosmosLaundromat.jpg',
|
||||
torrentFileName: 'cosmosLaundromat.torrent'
|
||||
},
|
||||
{
|
||||
testID: 'sintel',
|
||||
name: 'Sintel',
|
||||
posterFileName: 'sintel.jpg',
|
||||
torrentFileName: 'sintel.torrent'
|
||||
},
|
||||
{
|
||||
testID: 'tears',
|
||||
name: 'Tears of Steel',
|
||||
posterFileName: 'tearsOfSteel.jpg',
|
||||
torrentFileName: 'tearsOfSteel.torrent'
|
||||
},
|
||||
{
|
||||
testID: 'wired',
|
||||
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
||||
posterFileName: 'wiredCd.jpg',
|
||||
torrentFileName: 'wiredCd.torrent'
|
||||
@@ -62,8 +76,11 @@ module.exports = {
|
||||
|
||||
HOME_PAGE_URL: 'https://webtorrent.io',
|
||||
|
||||
IS_PORTABLE: isPortable(),
|
||||
IS_PRODUCTION: isProduction(),
|
||||
IS_PORTABLE: IS_PORTABLE,
|
||||
IS_PRODUCTION: IS_PRODUCTION,
|
||||
IS_TEST: IS_TEST,
|
||||
|
||||
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
|
||||
|
||||
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||
ROOT_PATH: path.join(__dirname, '..'),
|
||||
@@ -74,12 +91,19 @@ module.exports = {
|
||||
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
|
||||
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
|
||||
|
||||
WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents
|
||||
WINDOW_MIN_WIDTH: 425
|
||||
WINDOW_INITIAL_BOUNDS: {
|
||||
width: 500,
|
||||
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
|
||||
},
|
||||
WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents
|
||||
WINDOW_MIN_WIDTH: 425,
|
||||
|
||||
UI_HEADER_HEIGHT: UI_HEADER_HEIGHT,
|
||||
UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT
|
||||
}
|
||||
|
||||
function getConfigPath () {
|
||||
if (isPortable()) {
|
||||
if (IS_PORTABLE) {
|
||||
return PORTABLE_PATH
|
||||
} else {
|
||||
return path.dirname(appConfig.filePath)
|
||||
@@ -87,24 +111,47 @@ function getConfigPath () {
|
||||
}
|
||||
|
||||
function getDefaultDownloadPath () {
|
||||
if (!process || !process.type) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isPortable()) {
|
||||
if (IS_PORTABLE) {
|
||||
return path.join(getConfigPath(), 'Downloads')
|
||||
} else {
|
||||
return getPath('downloads')
|
||||
}
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
function getPath (key) {
|
||||
if (!process.versions.electron) {
|
||||
// Node.js process
|
||||
return ''
|
||||
} else if (process.type === 'renderer') {
|
||||
// Electron renderer process
|
||||
return electron.remote.app.getPath(key)
|
||||
} else {
|
||||
// Electron main process
|
||||
return electron.app.getPath(key)
|
||||
}
|
||||
}
|
||||
|
||||
return process.type === 'renderer'
|
||||
? electron.remote.app.getPath('downloads')
|
||||
: electron.app.getPath('downloads')
|
||||
function isTest () {
|
||||
return process.env.NODE_ENV === 'test'
|
||||
}
|
||||
|
||||
function isPortable () {
|
||||
if (IS_TEST) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (process.platform !== 'win32' || !IS_PRODUCTION) {
|
||||
// Fast path: Non-Windows platforms should not check for path on disk
|
||||
return false
|
||||
}
|
||||
|
||||
const fs = require('fs')
|
||||
|
||||
try {
|
||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
||||
// This line throws if the "Portable Settings" folder does not exist, and does
|
||||
// nothing otherwise.
|
||||
fs.accessSync(PORTABLE_PATH, fs.constants.R_OK | fs.constants.W_OK)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
@@ -112,6 +159,7 @@ function isPortable () {
|
||||
|
||||
function isProduction () {
|
||||
if (!process.versions.electron) {
|
||||
// Node.js process
|
||||
return false
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
|
||||
@@ -2,10 +2,10 @@ module.exports = {
|
||||
init
|
||||
}
|
||||
|
||||
var config = require('./config')
|
||||
var electron = require('electron')
|
||||
|
||||
function init () {
|
||||
const config = require('./config')
|
||||
const electron = require('electron')
|
||||
|
||||
electron.crashReporter.start({
|
||||
companyName: config.APP_NAME,
|
||||
productName: config.APP_NAME,
|
||||
|
||||
@@ -2,12 +2,12 @@ module.exports = {
|
||||
init
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
const config = require('../config')
|
||||
const log = require('./log')
|
||||
|
||||
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
||||
const ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
||||
'?version=' + config.APP_VERSION +
|
||||
'&platform=' + process.platform
|
||||
|
||||
@@ -26,7 +26,7 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
||||
* }
|
||||
*/
|
||||
function init () {
|
||||
var get = require('simple-get')
|
||||
const get = require('simple-get')
|
||||
get.concat(ANNOUNCEMENT_URL, onResponse)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ module.exports = {
|
||||
openFiles
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
|
||||
var log = require('./log')
|
||||
var windows = require('./windows')
|
||||
const log = require('./log')
|
||||
const windows = require('./windows')
|
||||
|
||||
/**
|
||||
* Show open dialog to create a single-file torrent.
|
||||
@@ -17,16 +17,11 @@ var windows = require('./windows')
|
||||
function openSeedFile () {
|
||||
if (!windows.main.win) return
|
||||
log('openSeedFile')
|
||||
var opts = {
|
||||
const opts = {
|
||||
title: 'Select a file for the torrent.',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
showOpenSeed(opts)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -37,7 +32,7 @@ function openSeedFile () {
|
||||
function openSeedDirectory () {
|
||||
if (!windows.main.win) return
|
||||
log('openSeedDirectory')
|
||||
var opts = process.platform === 'darwin'
|
||||
const opts = process.platform === 'darwin'
|
||||
? {
|
||||
title: 'Select a file or folder for the torrent.',
|
||||
properties: [ 'openFile', 'openDirectory' ]
|
||||
@@ -46,12 +41,7 @@ function openSeedDirectory () {
|
||||
title: 'Select a folder for the torrent.',
|
||||
properties: [ 'openDirectory' ]
|
||||
}
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
showOpenSeed(opts)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -61,7 +51,7 @@ function openSeedDirectory () {
|
||||
function openFiles () {
|
||||
if (!windows.main.win) return
|
||||
log('openFiles')
|
||||
var opts = process.platform === 'darwin'
|
||||
const opts = process.platform === 'darwin'
|
||||
? {
|
||||
title: 'Select a file or folder to add.',
|
||||
properties: [ 'openFile', 'openDirectory' ]
|
||||
@@ -84,7 +74,7 @@ function openFiles () {
|
||||
function openTorrentFile () {
|
||||
if (!windows.main.win) return
|
||||
log('openTorrentFile')
|
||||
var opts = {
|
||||
const opts = {
|
||||
title: 'Select a .torrent file.',
|
||||
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
||||
properties: [ 'openFile', 'multiSelections' ]
|
||||
@@ -119,3 +109,16 @@ function setTitle (title) {
|
||||
function resetTitle () {
|
||||
windows.main.dispatch('resetTitle')
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops up an Open File dialog with the given options.
|
||||
* After the user selects files / folders, shows the Create Torrent page.
|
||||
*/
|
||||
function showOpenSeed (opts) {
|
||||
setTitle(opts.title)
|
||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||
resetTitle()
|
||||
if (!Array.isArray(selectedPaths)) return
|
||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,19 +4,17 @@ module.exports = {
|
||||
setBadge
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
const {app, Menu} = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
|
||||
var dialog = require('./dialog')
|
||||
var log = require('./log')
|
||||
const dialog = require('./dialog')
|
||||
const log = require('./log')
|
||||
|
||||
/**
|
||||
* Add a right-click menu to the dock icon. (Mac)
|
||||
*/
|
||||
function init () {
|
||||
if (!app.dock) return
|
||||
var menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
const menu = Menu.buildFromTemplate(getMenuTemplate())
|
||||
app.dock.setMenu(menu)
|
||||
}
|
||||
|
||||
@@ -33,8 +31,11 @@ function downloadFinished (path) {
|
||||
* Display a counter badge for the app. (Mac, Linux)
|
||||
*/
|
||||
function setBadge (count) {
|
||||
log(`setBadge: ${count}`)
|
||||
app.setBadgeCount(Number(count))
|
||||
if (process.platform === 'darwin' ||
|
||||
process.platform === 'linux' && app.isUnityRunning()) {
|
||||
log(`setBadge: ${count}`)
|
||||
app.setBadgeCount(Number(count))
|
||||
}
|
||||
}
|
||||
|
||||
function getMenuTemplate () {
|
||||
|
||||
@@ -4,29 +4,36 @@ module.exports = {
|
||||
checkInstall
|
||||
}
|
||||
|
||||
var cp = require('child_process')
|
||||
var vlcCommand = require('vlc-command')
|
||||
const cp = require('child_process')
|
||||
const path = require('path')
|
||||
const vlcCommand = require('vlc-command')
|
||||
|
||||
var log = require('./log')
|
||||
var windows = require('./windows')
|
||||
const log = require('./log')
|
||||
const windows = require('./windows')
|
||||
|
||||
// holds a ChildProcess while we're playing a video in an external player, null otherwise
|
||||
var proc
|
||||
let proc = null
|
||||
|
||||
function checkInstall (path, cb) {
|
||||
function checkInstall (playerPath, cb) {
|
||||
// check for VLC if external player has not been specified by the user
|
||||
// otherwise assume the player is installed
|
||||
if (path == null) return vlcCommand((err) => cb(!err))
|
||||
if (playerPath == null) return vlcCommand((err) => cb(!err))
|
||||
process.nextTick(() => cb(true))
|
||||
}
|
||||
|
||||
function spawn (path, url, title) {
|
||||
if (path != null) return spawnExternal(path, [url])
|
||||
function spawn (playerPath, url, title) {
|
||||
if (playerPath != null) return spawnExternal(playerPath, [url])
|
||||
|
||||
// Try to find and use VLC if external player is not specified
|
||||
vlcCommand(function (err, vlcPath) {
|
||||
if (err) return windows.main.dispatch('externalPlayerNotFound')
|
||||
var args = ['--play-and-exit', '--video-on-top', '--quiet', `--meta-title=${JSON.stringify(title)}`, url]
|
||||
const args = [
|
||||
'--play-and-exit',
|
||||
'--video-on-top',
|
||||
'--quiet',
|
||||
`--meta-title=${JSON.stringify(title)}`,
|
||||
url
|
||||
]
|
||||
spawnExternal(vlcPath, args)
|
||||
})
|
||||
}
|
||||
@@ -38,13 +45,18 @@ function kill () {
|
||||
proc = null
|
||||
}
|
||||
|
||||
function spawnExternal (path, args) {
|
||||
log('Running external media player:', path + ' ' + args.join(' '))
|
||||
function spawnExternal (playerPath, args) {
|
||||
log('Running external media player:', playerPath + ' ' + args.join(' '))
|
||||
|
||||
proc = cp.spawn(path, args, {stdio: 'ignore'})
|
||||
if (process.platform === 'darwin' && path.extname(playerPath) === '.app') {
|
||||
// Mac: Use executable in packaged .app bundle
|
||||
playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
|
||||
}
|
||||
|
||||
proc = cp.spawn(playerPath, args, {stdio: 'ignore'})
|
||||
|
||||
// If it works, close the modal after a second
|
||||
var closeModalTimeout = setTimeout(() =>
|
||||
const closeModalTimeout = setTimeout(() =>
|
||||
windows.main.dispatch('exitModal'), 1000)
|
||||
|
||||
proc.on('close', function (code) {
|
||||
|
||||
@@ -3,8 +3,8 @@ module.exports = {
|
||||
uninstall
|
||||
}
|
||||
|
||||
var config = require('../config')
|
||||
var path = require('path')
|
||||
const config = require('../config')
|
||||
const path = require('path')
|
||||
|
||||
function install () {
|
||||
if (process.platform === 'darwin') {
|
||||
@@ -31,8 +31,8 @@ function uninstall () {
|
||||
}
|
||||
|
||||
function installDarwin () {
|
||||
var electron = require('electron')
|
||||
var app = electron.app
|
||||
const electron = require('electron')
|
||||
const app = electron.app
|
||||
|
||||
// On Mac, only protocols that are listed in `Info.plist` can be set as the
|
||||
// default handler at runtime.
|
||||
@@ -44,18 +44,18 @@ function installDarwin () {
|
||||
|
||||
function uninstallDarwin () {}
|
||||
|
||||
var EXEC_COMMAND = [ process.execPath ]
|
||||
const EXEC_COMMAND = [ process.execPath ]
|
||||
|
||||
if (!config.IS_PRODUCTION) {
|
||||
EXEC_COMMAND.push(config.ROOT_PATH)
|
||||
}
|
||||
|
||||
function installWin32 () {
|
||||
var Registry = require('winreg')
|
||||
const Registry = require('winreg')
|
||||
|
||||
var log = require('./log')
|
||||
const log = require('./log')
|
||||
|
||||
var iconPath = path.join(
|
||||
const iconPath = path.join(
|
||||
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
|
||||
)
|
||||
registerProtocolHandlerWin32(
|
||||
@@ -100,7 +100,7 @@ function installWin32 () {
|
||||
*/
|
||||
|
||||
function registerProtocolHandlerWin32 (protocol, name, icon, command) {
|
||||
var protocolKey = new Registry({
|
||||
const protocolKey = new Registry({
|
||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||
key: '\\Software\\Classes\\' + protocol
|
||||
})
|
||||
@@ -120,7 +120,7 @@ function installWin32 () {
|
||||
function setIcon (err) {
|
||||
if (err) log.error(err.message)
|
||||
|
||||
var iconKey = new Registry({
|
||||
const iconKey = new Registry({
|
||||
hive: Registry.HKCU,
|
||||
key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
|
||||
})
|
||||
@@ -130,7 +130,7 @@ function installWin32 () {
|
||||
function setCommand (err) {
|
||||
if (err) log.error(err.message)
|
||||
|
||||
var commandKey = new Registry({
|
||||
const commandKey = new Registry({
|
||||
hive: Registry.HKCU,
|
||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
||||
})
|
||||
@@ -161,7 +161,7 @@ function installWin32 () {
|
||||
setExt()
|
||||
|
||||
function setExt () {
|
||||
var extKey = new Registry({
|
||||
const extKey = new Registry({
|
||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||
key: '\\Software\\Classes\\' + ext
|
||||
})
|
||||
@@ -171,7 +171,7 @@ function installWin32 () {
|
||||
function setId (err) {
|
||||
if (err) log.error(err.message)
|
||||
|
||||
var idKey = new Registry({
|
||||
const idKey = new Registry({
|
||||
hive: Registry.HKCU,
|
||||
key: '\\Software\\Classes\\' + id
|
||||
})
|
||||
@@ -181,7 +181,7 @@ function installWin32 () {
|
||||
function setIcon (err) {
|
||||
if (err) log.error(err.message)
|
||||
|
||||
var iconKey = new Registry({
|
||||
const iconKey = new Registry({
|
||||
hive: Registry.HKCU,
|
||||
key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
|
||||
})
|
||||
@@ -191,7 +191,7 @@ function installWin32 () {
|
||||
function setCommand (err) {
|
||||
if (err) log.error(err.message)
|
||||
|
||||
var commandKey = new Registry({
|
||||
const commandKey = new Registry({
|
||||
hive: Registry.HKCU,
|
||||
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
|
||||
})
|
||||
@@ -205,7 +205,7 @@ function installWin32 () {
|
||||
}
|
||||
|
||||
function uninstallWin32 () {
|
||||
var Registry = require('winreg')
|
||||
const Registry = require('winreg')
|
||||
|
||||
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
||||
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
|
||||
@@ -215,7 +215,7 @@ function uninstallWin32 () {
|
||||
getCommand()
|
||||
|
||||
function getCommand () {
|
||||
var commandKey = new Registry({
|
||||
const commandKey = new Registry({
|
||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
||||
})
|
||||
@@ -227,7 +227,7 @@ function uninstallWin32 () {
|
||||
}
|
||||
|
||||
function destroyProtocol () {
|
||||
var protocolKey = new Registry({
|
||||
const protocolKey = new Registry({
|
||||
hive: Registry.HKCU,
|
||||
key: '\\Software\\Classes\\' + protocol
|
||||
})
|
||||
@@ -239,7 +239,7 @@ function uninstallWin32 () {
|
||||
eraseId()
|
||||
|
||||
function eraseId () {
|
||||
var idKey = new Registry({
|
||||
const idKey = new Registry({
|
||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||
key: '\\Software\\Classes\\' + id
|
||||
})
|
||||
@@ -247,7 +247,7 @@ function uninstallWin32 () {
|
||||
}
|
||||
|
||||
function getExt () {
|
||||
var extKey = new Registry({
|
||||
const extKey = new Registry({
|
||||
hive: Registry.HKCU,
|
||||
key: '\\Software\\Classes\\' + ext
|
||||
})
|
||||
@@ -259,7 +259,7 @@ function uninstallWin32 () {
|
||||
}
|
||||
|
||||
function destroyExt () {
|
||||
var extKey = new Registry({
|
||||
const extKey = new Registry({
|
||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||
key: '\\Software\\Classes\\' + ext
|
||||
})
|
||||
@@ -273,12 +273,12 @@ function commandToArgs (command) {
|
||||
}
|
||||
|
||||
function installLinux () {
|
||||
var fs = require('fs-extra')
|
||||
var os = require('os')
|
||||
var path = require('path')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
const config = require('../config')
|
||||
const log = require('./log')
|
||||
|
||||
// Do not install in user dir if running on system
|
||||
if (/^\/opt/.test(process.execPath)) return
|
||||
@@ -287,7 +287,7 @@ function installLinux () {
|
||||
installIconFile()
|
||||
|
||||
function installDesktopFile () {
|
||||
var templatePath = path.join(
|
||||
const templatePath = path.join(
|
||||
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
|
||||
)
|
||||
fs.readFile(templatePath, 'utf8', writeDesktopFile)
|
||||
@@ -296,7 +296,7 @@ function installLinux () {
|
||||
function writeDesktopFile (err, desktopFile) {
|
||||
if (err) return log.error(err.message)
|
||||
|
||||
var appPath = config.IS_PRODUCTION
|
||||
const appPath = config.IS_PRODUCTION
|
||||
? path.dirname(process.execPath)
|
||||
: config.ROOT_PATH
|
||||
|
||||
@@ -305,7 +305,7 @@ function installLinux () {
|
||||
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
|
||||
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
|
||||
|
||||
var desktopFilePath = path.join(
|
||||
const desktopFilePath = path.join(
|
||||
os.homedir(),
|
||||
'.local',
|
||||
'share',
|
||||
@@ -319,47 +319,51 @@ function installLinux () {
|
||||
}
|
||||
|
||||
function installIconFile () {
|
||||
var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
|
||||
const iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
|
||||
fs.readFile(iconStaticPath, writeIconFile)
|
||||
}
|
||||
|
||||
function writeIconFile (err, iconFile) {
|
||||
if (err) return log.error(err.message)
|
||||
|
||||
var iconFilePath = path.join(
|
||||
const mkdirp = require('mkdirp')
|
||||
|
||||
const iconFilePath = path.join(
|
||||
os.homedir(),
|
||||
'.local',
|
||||
'share',
|
||||
'icons',
|
||||
'webtorrent-desktop.png'
|
||||
)
|
||||
fs.mkdirp(path.dirname(iconFilePath))
|
||||
fs.writeFile(iconFilePath, iconFile, function (err) {
|
||||
mkdirp(path.dirname(iconFilePath), (err) => {
|
||||
if (err) return log.error(err.message)
|
||||
fs.writeFile(iconFilePath, iconFile, (err) => {
|
||||
if (err) log.error(err.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function uninstallLinux () {
|
||||
var os = require('os')
|
||||
var path = require('path')
|
||||
var fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const rimraf = require('rimraf')
|
||||
|
||||
var desktopFilePath = path.join(
|
||||
const desktopFilePath = path.join(
|
||||
os.homedir(),
|
||||
'.local',
|
||||
'share',
|
||||
'applications',
|
||||
'webtorrent-desktop.desktop'
|
||||
)
|
||||
fs.removeSync(desktopFilePath)
|
||||
rimraf(desktopFilePath)
|
||||
|
||||
var iconFilePath = path.join(
|
||||
const iconFilePath = path.join(
|
||||
os.homedir(),
|
||||
'.local',
|
||||
'share',
|
||||
'icons',
|
||||
'webtorrent-desktop.png'
|
||||
)
|
||||
fs.removeSync(iconFilePath)
|
||||
rimraf(iconFilePath)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
console.time('init')
|
||||
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
const app = electron.app
|
||||
|
||||
var app = electron.app
|
||||
var ipcMain = electron.ipcMain
|
||||
const parallel = require('run-parallel')
|
||||
|
||||
var announcement = require('./announcement')
|
||||
var config = require('../config')
|
||||
var crashReporter = require('../crash-reporter')
|
||||
var dialog = require('./dialog')
|
||||
var dock = require('./dock')
|
||||
var ipc = require('./ipc')
|
||||
var log = require('./log')
|
||||
var menu = require('./menu')
|
||||
var squirrelWin32 = require('./squirrel-win32')
|
||||
var tray = require('./tray')
|
||||
var updater = require('./updater')
|
||||
var userTasks = require('./user-tasks')
|
||||
var windows = require('./windows')
|
||||
const config = require('../config')
|
||||
const crashReporter = require('../crash-reporter')
|
||||
const ipc = require('./ipc')
|
||||
const log = require('./log')
|
||||
const menu = require('./menu')
|
||||
const State = require('../renderer/lib/state')
|
||||
const windows = require('./windows')
|
||||
|
||||
var shouldQuit = false
|
||||
var argv = sliceArgv(process.argv)
|
||||
let shouldQuit = false
|
||||
let argv = sliceArgv(process.argv)
|
||||
|
||||
// Start the app without showing the main window when auto launching on login
|
||||
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
|
||||
const hidden = argv.includes('--hidden') ||
|
||||
(process.platform === 'darwin' && app.getLoginItemSettings().wasOpenedAsHidden)
|
||||
|
||||
if (config.IS_PRODUCTION) {
|
||||
// When Electron is running in production mode (packaged app), then run React
|
||||
@@ -29,13 +28,16 @@ if (config.IS_PRODUCTION) {
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const squirrelWin32 = require('./squirrel-win32')
|
||||
shouldQuit = squirrelWin32.handleEvent(argv[0])
|
||||
argv = argv.filter((arg) => !arg.includes('--squirrel'))
|
||||
}
|
||||
|
||||
if (!shouldQuit) {
|
||||
if (!shouldQuit && !config.IS_PORTABLE) {
|
||||
// Prevent multiple instances of app from running at same time. New instances
|
||||
// signal this instance and quit.
|
||||
// signal this instance and quit. Note: This feature creates a lock file in
|
||||
// %APPDATA%\Roaming\WebTorrent so we do not do it for the Portable App since
|
||||
// we want to be "silent" as well as "portable".
|
||||
shouldQuit = app.makeSingleInstance(onAppOpen)
|
||||
if (shouldQuit) {
|
||||
app.quit()
|
||||
@@ -48,27 +50,30 @@ if (!shouldQuit) {
|
||||
|
||||
function init () {
|
||||
if (config.IS_PORTABLE) {
|
||||
const path = require('path')
|
||||
// Put all user data into the "Portable Settings" folder
|
||||
app.setPath('userData', config.CONFIG_PATH)
|
||||
// Put Electron crash files, etc. into the "Portable Settings\Temp" folder
|
||||
app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp'))
|
||||
}
|
||||
|
||||
var isReady = false // app ready, windows can be created
|
||||
const ipcMain = electron.ipcMain
|
||||
|
||||
let isReady = false // app ready, windows can be created
|
||||
app.ipcReady = false // main window has finished loading and IPC is ready
|
||||
app.isQuitting = false
|
||||
|
||||
// Open handlers must be added as early as possible
|
||||
app.on('open-file', onOpen)
|
||||
app.on('open-url', onOpen)
|
||||
parallel({
|
||||
appReady: (cb) => app.on('ready', () => cb(null)),
|
||||
state: (cb) => State.load(cb)
|
||||
}, onReady)
|
||||
|
||||
ipc.init()
|
||||
function onReady (err, results) {
|
||||
if (err) throw err
|
||||
|
||||
app.once('will-finish-launching', function () {
|
||||
crashReporter.init()
|
||||
})
|
||||
|
||||
app.on('ready', function () {
|
||||
isReady = true
|
||||
|
||||
windows.main.init()
|
||||
windows.main.init(results.state, {hidden: hidden})
|
||||
windows.webtorrent.init()
|
||||
menu.init()
|
||||
|
||||
@@ -78,9 +83,18 @@ function init () {
|
||||
// Report uncaught exceptions
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error(err)
|
||||
var errJSON = {message: err.message, stack: err.stack}
|
||||
windows.main.dispatch('uncaughtError', 'main', errJSON)
|
||||
const error = {message: err.message, stack: err.stack}
|
||||
windows.main.dispatch('uncaughtError', 'main', error)
|
||||
})
|
||||
}
|
||||
|
||||
app.on('open-file', onOpen)
|
||||
app.on('open-url', onOpen)
|
||||
|
||||
ipc.init()
|
||||
|
||||
app.once('will-finish-launching', function () {
|
||||
crashReporter.init()
|
||||
})
|
||||
|
||||
app.once('ipcReady', function () {
|
||||
@@ -94,12 +108,12 @@ function init () {
|
||||
|
||||
app.isQuitting = true
|
||||
e.preventDefault()
|
||||
windows.main.dispatch('saveState') // try to save state on exit
|
||||
ipcMain.once('savedState', () => app.quit())
|
||||
windows.main.dispatch('stateSaveImmediate') // try to save state on exit
|
||||
ipcMain.once('stateSaved', () => app.quit())
|
||||
setTimeout(() => {
|
||||
console.error('Saving state took too long. Quitting.')
|
||||
app.quit()
|
||||
}, 2000) // quit after 2 secs, at most
|
||||
}, 4000) // quit after 4 secs, at most
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
@@ -108,11 +122,25 @@ function init () {
|
||||
}
|
||||
|
||||
function delayedInit () {
|
||||
if (app.isQuitting) return
|
||||
|
||||
const announcement = require('./announcement')
|
||||
const dock = require('./dock')
|
||||
const updater = require('./updater')
|
||||
|
||||
announcement.init()
|
||||
dock.init()
|
||||
tray.init()
|
||||
updater.init()
|
||||
userTasks.init()
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const userTasks = require('./user-tasks')
|
||||
userTasks.init()
|
||||
}
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
const tray = require('./tray')
|
||||
tray.init()
|
||||
}
|
||||
}
|
||||
|
||||
function onOpen (e, torrentId) {
|
||||
@@ -143,22 +171,38 @@ function onAppOpen (newArgv) {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading args.
|
||||
// Production: 1 arg, eg: /Applications/WebTorrent.app/Contents/MacOS/WebTorrent
|
||||
// Development: 2 args, eg: electron .
|
||||
// Test: 4 args, eg: electron -r .../mocks.js .
|
||||
function sliceArgv (argv) {
|
||||
return argv.slice(config.IS_PRODUCTION ? 1 : 2)
|
||||
return argv.slice(config.IS_PRODUCTION ? 1
|
||||
: config.IS_TEST ? 4
|
||||
: 2)
|
||||
}
|
||||
|
||||
function processArgv (argv) {
|
||||
var torrentIds = []
|
||||
let torrentIds = []
|
||||
argv.forEach(function (arg) {
|
||||
if (arg === '-n') {
|
||||
dialog.openSeedDirectory()
|
||||
} else if (arg === '-o') {
|
||||
dialog.openTorrentFile()
|
||||
} else if (arg === '-u') {
|
||||
dialog.openTorrentAddress()
|
||||
if (arg === '-n' || arg === '-o' || arg === '-u') {
|
||||
// Critical path: Only load the 'dialog' package if it is needed
|
||||
const dialog = require('./dialog')
|
||||
if (arg === '-n') {
|
||||
dialog.openSeedDirectory()
|
||||
} else if (arg === '-o') {
|
||||
dialog.openTorrentFile()
|
||||
} else if (arg === '-u') {
|
||||
dialog.openTorrentAddress()
|
||||
}
|
||||
} else if (arg === '--hidden') {
|
||||
// Ignore hidden argument, already being handled
|
||||
} else if (arg.startsWith('-psn')) {
|
||||
// Ignore Mac launchd "process serial number" argument
|
||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
||||
} else if (arg.startsWith('--')) {
|
||||
// Ignore Spectron flags
|
||||
} else if (arg === 'data:,') {
|
||||
// Ignore weird Spectron argument
|
||||
} else if (arg !== '.') {
|
||||
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
|
||||
// development copy of WebTorrent is started while a production version is
|
||||
|
||||
122
src/main/ipc.js
@@ -2,27 +2,19 @@ module.exports = {
|
||||
init
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
const app = electron.app
|
||||
|
||||
var dialog = require('./dialog')
|
||||
var dock = require('./dock')
|
||||
var handlers = require('./handlers')
|
||||
var log = require('./log')
|
||||
var menu = require('./menu')
|
||||
var powerSaveBlocker = require('./power-save-blocker')
|
||||
var shell = require('./shell')
|
||||
var shortcuts = require('./shortcuts')
|
||||
var externalPlayer = require('./external-player')
|
||||
var windows = require('./windows')
|
||||
var thumbar = require('./thumbar')
|
||||
const log = require('./log')
|
||||
const menu = require('./menu')
|
||||
const windows = require('./windows')
|
||||
|
||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||
var messageQueueMainToWebTorrent = []
|
||||
const messageQueueMainToWebTorrent = []
|
||||
|
||||
function init () {
|
||||
var ipc = electron.ipcMain
|
||||
const ipc = electron.ipcMain
|
||||
|
||||
ipc.once('ipcReady', function (e) {
|
||||
app.ipcReady = true
|
||||
@@ -43,40 +35,73 @@ function init () {
|
||||
* Dialog
|
||||
*/
|
||||
|
||||
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
|
||||
ipc.on('openFiles', () => dialog.openFiles())
|
||||
ipc.on('openTorrentFile', () => {
|
||||
const dialog = require('./dialog')
|
||||
dialog.openTorrentFile()
|
||||
})
|
||||
ipc.on('openFiles', () => {
|
||||
const dialog = require('./dialog')
|
||||
dialog.openFiles()
|
||||
})
|
||||
|
||||
/**
|
||||
* Dock
|
||||
*/
|
||||
|
||||
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
|
||||
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
|
||||
ipc.on('setBadge', (e, ...args) => {
|
||||
const dock = require('./dock')
|
||||
dock.setBadge(...args)
|
||||
})
|
||||
ipc.on('downloadFinished', (e, ...args) => {
|
||||
const dock = require('./dock')
|
||||
dock.downloadFinished(...args)
|
||||
})
|
||||
|
||||
/**
|
||||
* Events
|
||||
*/
|
||||
|
||||
ipc.on('onPlayerOpen', function () {
|
||||
menu.setPlayerOpen(true)
|
||||
const powerSaveBlocker = require('./power-save-blocker')
|
||||
const shortcuts = require('./shortcuts')
|
||||
const thumbar = require('./thumbar')
|
||||
|
||||
menu.togglePlaybackControls(true)
|
||||
powerSaveBlocker.enable()
|
||||
shortcuts.enable()
|
||||
thumbar.enable()
|
||||
})
|
||||
|
||||
ipc.on('onPlayerUpdate', function (e, ...args) {
|
||||
const thumbar = require('./thumbar')
|
||||
|
||||
menu.onPlayerUpdate(...args)
|
||||
thumbar.onPlayerUpdate(...args)
|
||||
})
|
||||
|
||||
ipc.on('onPlayerClose', function () {
|
||||
menu.setPlayerOpen(false)
|
||||
const powerSaveBlocker = require('./power-save-blocker')
|
||||
const shortcuts = require('./shortcuts')
|
||||
const thumbar = require('./thumbar')
|
||||
|
||||
menu.togglePlaybackControls(false)
|
||||
powerSaveBlocker.disable()
|
||||
shortcuts.disable()
|
||||
thumbar.disable()
|
||||
})
|
||||
|
||||
ipc.on('onPlayerPlay', function () {
|
||||
const powerSaveBlocker = require('./power-save-blocker')
|
||||
const thumbar = require('./thumbar')
|
||||
|
||||
powerSaveBlocker.enable()
|
||||
thumbar.onPlayerPlay()
|
||||
})
|
||||
|
||||
ipc.on('onPlayerPause', function () {
|
||||
const powerSaveBlocker = require('./power-save-blocker')
|
||||
const thumbar = require('./thumbar')
|
||||
|
||||
powerSaveBlocker.disable()
|
||||
thumbar.onPlayerPause()
|
||||
})
|
||||
@@ -85,23 +110,46 @@ function init () {
|
||||
* Shell
|
||||
*/
|
||||
|
||||
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
||||
ipc.on('openItem', (e, ...args) => {
|
||||
const shell = require('./shell')
|
||||
shell.openItem(...args)
|
||||
})
|
||||
ipc.on('showItemInFolder', (e, ...args) => {
|
||||
const shell = require('./shell')
|
||||
shell.showItemInFolder(...args)
|
||||
})
|
||||
ipc.on('moveItemToTrash', (e, ...args) => {
|
||||
const shell = require('./shell')
|
||||
shell.moveItemToTrash(...args)
|
||||
})
|
||||
|
||||
/**
|
||||
* File handlers
|
||||
*/
|
||||
|
||||
ipc.on('setDefaultFileHandler', (e, flag) => {
|
||||
const handlers = require('./handlers')
|
||||
|
||||
if (flag) handlers.install()
|
||||
else handlers.uninstall()
|
||||
})
|
||||
|
||||
/**
|
||||
* Auto start on login
|
||||
*/
|
||||
|
||||
ipc.on('setStartup', (e, flag) => {
|
||||
const startup = require('./startup')
|
||||
|
||||
if (flag) startup.install()
|
||||
else startup.uninstall()
|
||||
})
|
||||
|
||||
/**
|
||||
* Windows: Main
|
||||
*/
|
||||
|
||||
var main = windows.main
|
||||
const main = windows.main
|
||||
|
||||
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
|
||||
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
|
||||
@@ -116,16 +164,32 @@ function init () {
|
||||
*/
|
||||
|
||||
ipc.on('checkForExternalPlayer', function (e, path) {
|
||||
const externalPlayer = require('./external-player')
|
||||
|
||||
externalPlayer.checkInstall(path, function (isInstalled) {
|
||||
windows.main.send('checkForExternalPlayer', isInstalled)
|
||||
})
|
||||
})
|
||||
|
||||
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
|
||||
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
||||
ipc.on('openExternalPlayer', (e, ...args) => {
|
||||
const externalPlayer = require('./external-player')
|
||||
const thumbar = require('./thumbar')
|
||||
|
||||
// Capture all events
|
||||
var oldEmit = ipc.emit
|
||||
menu.togglePlaybackControls(false)
|
||||
thumbar.disable()
|
||||
externalPlayer.spawn(...args)
|
||||
})
|
||||
|
||||
ipc.on('quitExternalPlayer', () => {
|
||||
const externalPlayer = require('./external-player')
|
||||
externalPlayer.kill()
|
||||
})
|
||||
|
||||
/**
|
||||
* Message passing
|
||||
*/
|
||||
|
||||
const oldEmit = ipc.emit
|
||||
ipc.emit = function (name, e, ...args) {
|
||||
// Relay messages between the main window and the WebTorrent hidden window
|
||||
if (name.startsWith('wt-') && !app.isQuitting) {
|
||||
|
||||
@@ -8,10 +8,10 @@ module.exports.error = error
|
||||
* where they can be viewed in Developer Tools.
|
||||
*/
|
||||
|
||||
var electron = require('electron')
|
||||
var windows = require('./windows')
|
||||
const electron = require('electron')
|
||||
const windows = require('./windows')
|
||||
|
||||
var app = electron.app
|
||||
const app = electron.app
|
||||
|
||||
function log (...args) {
|
||||
if (app.ipcReady) {
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
module.exports = {
|
||||
init,
|
||||
setPlayerOpen,
|
||||
togglePlaybackControls,
|
||||
setWindowFocus,
|
||||
setAllowNav,
|
||||
onPlayerUpdate,
|
||||
onToggleAlwaysOnTop,
|
||||
onToggleFullScreen
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
const app = electron.app
|
||||
|
||||
var config = require('../config')
|
||||
var dialog = require('./dialog')
|
||||
var shell = require('./shell')
|
||||
var windows = require('./windows')
|
||||
const config = require('../config')
|
||||
const windows = require('./windows')
|
||||
|
||||
var menu
|
||||
let menu = null
|
||||
|
||||
function init () {
|
||||
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
electron.Menu.setApplicationMenu(menu)
|
||||
}
|
||||
|
||||
function setPlayerOpen (flag) {
|
||||
function togglePlaybackControls (flag) {
|
||||
getMenuItem('Play/Pause').enabled = flag
|
||||
getMenuItem('Skip Next').enabled = flag
|
||||
getMenuItem('Skip Previous').enabled = flag
|
||||
getMenuItem('Increase Volume').enabled = flag
|
||||
getMenuItem('Decrease Volume').enabled = flag
|
||||
getMenuItem('Step Forward').enabled = flag
|
||||
@@ -32,6 +33,16 @@ function setPlayerOpen (flag) {
|
||||
getMenuItem('Increase Speed').enabled = flag
|
||||
getMenuItem('Decrease Speed').enabled = flag
|
||||
getMenuItem('Add Subtitles File...').enabled = flag
|
||||
|
||||
if (flag === false) {
|
||||
getMenuItem('Skip Next').enabled = false
|
||||
getMenuItem('Skip Previous').enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
function onPlayerUpdate (hasNext, hasPrevious) {
|
||||
getMenuItem('Skip Next').enabled = hasNext
|
||||
getMenuItem('Skip Previous').enabled = hasPrevious
|
||||
}
|
||||
|
||||
function setWindowFocus (flag) {
|
||||
@@ -42,9 +53,12 @@ function setWindowFocus (flag) {
|
||||
// Disallow opening more screens on top of the current one.
|
||||
function setAllowNav (flag) {
|
||||
getMenuItem('Preferences').enabled = flag
|
||||
getMenuItem('Create New Torrent...').enabled = flag
|
||||
var item = getMenuItem('Create New Torrent from File...')
|
||||
if (item) item.enabled = flag
|
||||
if (process.platform === 'darwin') {
|
||||
getMenuItem('Create New Torrent...').enabled = flag
|
||||
} else {
|
||||
getMenuItem('Create New Torrent from Folder...').enabled = flag
|
||||
getMenuItem('Create New Torrent from File...').enabled = flag
|
||||
}
|
||||
}
|
||||
|
||||
function onToggleAlwaysOnTop (flag) {
|
||||
@@ -56,8 +70,8 @@ function onToggleFullScreen (flag) {
|
||||
}
|
||||
|
||||
function getMenuItem (label) {
|
||||
for (var i = 0; i < menu.items.length; i++) {
|
||||
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||
for (let i = 0; i < menu.items.length; i++) {
|
||||
const menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||
return item.label === label
|
||||
})
|
||||
if (menuItem) return menuItem
|
||||
@@ -65,7 +79,7 @@ function getMenuItem (label) {
|
||||
}
|
||||
|
||||
function getMenuTemplate () {
|
||||
var template = [
|
||||
const template = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
@@ -74,17 +88,26 @@ function getMenuTemplate () {
|
||||
? 'Create New Torrent...'
|
||||
: 'Create New Torrent from Folder...',
|
||||
accelerator: 'CmdOrCtrl+N',
|
||||
click: () => dialog.openSeedDirectory()
|
||||
click: () => {
|
||||
const dialog = require('./dialog')
|
||||
dialog.openSeedDirectory()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent File...',
|
||||
accelerator: 'CmdOrCtrl+O',
|
||||
click: () => dialog.openTorrentFile()
|
||||
click: () => {
|
||||
const dialog = require('./dialog')
|
||||
dialog.openTorrentFile()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Open Torrent Address...',
|
||||
accelerator: 'CmdOrCtrl+U',
|
||||
click: () => dialog.openTorrentAddress()
|
||||
click: () => {
|
||||
const dialog = require('./dialog')
|
||||
dialog.openTorrentAddress()
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
@@ -184,6 +207,21 @@ function getMenuTemplate () {
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Skip Next',
|
||||
accelerator: 'N',
|
||||
click: () => windows.main.dispatch('nextTrack'),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Skip Previous',
|
||||
accelerator: 'P',
|
||||
click: () => windows.main.dispatch('previousTrack'),
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Increase Volume',
|
||||
accelerator: 'CmdOrCtrl+Up',
|
||||
@@ -246,18 +284,27 @@ function getMenuTemplate () {
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn more about ' + config.APP_NAME,
|
||||
click: () => shell.openExternal(config.HOME_PAGE_URL)
|
||||
click: () => {
|
||||
const shell = require('./shell')
|
||||
shell.openExternal(config.HOME_PAGE_URL)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Contribute on GitHub',
|
||||
click: () => shell.openExternal(config.GITHUB_URL)
|
||||
click: () => {
|
||||
const shell = require('./shell')
|
||||
shell.openExternal(config.GITHUB_URL)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Report an Issue...',
|
||||
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||
click: () => {
|
||||
const shell = require('./shell')
|
||||
shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -330,7 +377,10 @@ function getMenuTemplate () {
|
||||
// File menu (Windows, Linux)
|
||||
template[0].submenu.unshift({
|
||||
label: 'Create New Torrent from File...',
|
||||
click: () => dialog.openSeedFile()
|
||||
click: () => {
|
||||
const dialog = require('./dialog')
|
||||
dialog.openSeedFile()
|
||||
}
|
||||
})
|
||||
|
||||
// Edit menu (Windows, Linux)
|
||||
|
||||
@@ -3,10 +3,10 @@ module.exports = {
|
||||
disable
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
var log = require('./log')
|
||||
const electron = require('electron')
|
||||
const log = require('./log')
|
||||
|
||||
var blockId = 0
|
||||
let blockId = 0
|
||||
|
||||
/**
|
||||
* Block the system from entering low-power (sleep) mode or turning off the
|
||||
|
||||
@@ -5,8 +5,8 @@ module.exports = {
|
||||
moveItemToTrash
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
var log = require('./log')
|
||||
const electron = require('electron')
|
||||
const log = require('./log')
|
||||
|
||||
/**
|
||||
* Open the given external protocol URL in the desktop’s default manner.
|
||||
|
||||
@@ -3,8 +3,8 @@ module.exports = {
|
||||
enable
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
var windows = require('./windows')
|
||||
const electron = require('electron')
|
||||
const windows = require('./windows')
|
||||
|
||||
function enable () {
|
||||
// Register play/pause media key, available on some keyboards.
|
||||
@@ -12,9 +12,19 @@ function enable () {
|
||||
'MediaPlayPause',
|
||||
() => windows.main.dispatch('playPause')
|
||||
)
|
||||
electron.globalShortcut.register(
|
||||
'MediaNextTrack',
|
||||
() => windows.main.dispatch('nextTrack')
|
||||
)
|
||||
electron.globalShortcut.register(
|
||||
'MediaPreviousTrack',
|
||||
() => windows.main.dispatch('previousTrack')
|
||||
)
|
||||
}
|
||||
|
||||
function disable () {
|
||||
// Return the media key to the OS, so other apps can use it.
|
||||
electron.globalShortcut.unregister('MediaPlayPause')
|
||||
electron.globalShortcut.unregister('MediaNextTrack')
|
||||
electron.globalShortcut.unregister('MediaPreviousTrack')
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ module.exports = {
|
||||
handleEvent
|
||||
}
|
||||
|
||||
var cp = require('child_process')
|
||||
var electron = require('electron')
|
||||
var fs = require('fs')
|
||||
var os = require('os')
|
||||
var path = require('path')
|
||||
const cp = require('child_process')
|
||||
const electron = require('electron')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
var app = electron.app
|
||||
const app = electron.app
|
||||
|
||||
var handlers = require('./handlers')
|
||||
const handlers = require('./handlers')
|
||||
|
||||
var EXE_NAME = path.basename(process.execPath)
|
||||
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||
const EXE_NAME = path.basename(process.execPath)
|
||||
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||
|
||||
function handleEvent (cmd) {
|
||||
if (cmd === '--squirrel-install') {
|
||||
@@ -73,9 +73,9 @@ function handleEvent (cmd) {
|
||||
* the output from standard out.
|
||||
*/
|
||||
function spawn (command, args, cb) {
|
||||
var stdout = ''
|
||||
|
||||
var child
|
||||
let stdout = ''
|
||||
let error = null
|
||||
let child = null
|
||||
try {
|
||||
child = cp.spawn(command, args)
|
||||
} catch (err) {
|
||||
@@ -90,10 +90,10 @@ function spawn (command, args, cb) {
|
||||
stdout += data
|
||||
})
|
||||
|
||||
var error = null
|
||||
child.on('error', function (processError) {
|
||||
error = processError
|
||||
})
|
||||
|
||||
child.on('close', function (code, signal) {
|
||||
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
|
||||
if (error) error.stdout = stdout
|
||||
@@ -122,12 +122,12 @@ function createShortcuts (cb) {
|
||||
* command.
|
||||
*/
|
||||
function updateShortcuts (cb) {
|
||||
var homeDir = os.homedir()
|
||||
const homeDir = os.homedir()
|
||||
if (homeDir) {
|
||||
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
||||
const desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
||||
// If the desktop shortcut was deleted by the user, then keep it deleted.
|
||||
fs.access(desktopShortcutPath, function (err) {
|
||||
var desktopShortcutExists = !err
|
||||
const desktopShortcutExists = !err
|
||||
createShortcuts(function () {
|
||||
if (desktopShortcutExists) {
|
||||
cb()
|
||||
|
||||
36
src/main/startup.js
Normal file
@@ -0,0 +1,36 @@
|
||||
module.exports = {
|
||||
install,
|
||||
uninstall
|
||||
}
|
||||
|
||||
const config = require('../config')
|
||||
const AutoLaunch = require('auto-launch')
|
||||
const { app } = require('electron')
|
||||
|
||||
// On Mac, work around a bug in auto-launch where it opens a Terminal window
|
||||
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
|
||||
const appPath = process.platform === 'darwin'
|
||||
? app.getPath('exe').replace(/\.app\/Content.*/, '.app')
|
||||
: undefined // Use the default
|
||||
|
||||
const appLauncher = new AutoLaunch({
|
||||
name: config.APP_NAME,
|
||||
path: appPath,
|
||||
isHidden: true
|
||||
})
|
||||
|
||||
function install () {
|
||||
return appLauncher
|
||||
.isEnabled()
|
||||
.then(enabled => {
|
||||
if (!enabled) return appLauncher.enable()
|
||||
})
|
||||
}
|
||||
|
||||
function uninstall () {
|
||||
return appLauncher
|
||||
.isEnabled()
|
||||
.then(enabled => {
|
||||
if (enabled) return appLauncher.disable()
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,8 @@ module.exports = {
|
||||
disable,
|
||||
enable,
|
||||
onPlayerPause,
|
||||
onPlayerPlay
|
||||
onPlayerPlay,
|
||||
onPlayerUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -11,44 +12,80 @@ module.exports = {
|
||||
* or activating the window.
|
||||
*/
|
||||
|
||||
var path = require('path')
|
||||
var config = require('../config')
|
||||
const path = require('path')
|
||||
const config = require('../config')
|
||||
|
||||
var windows = require('./windows')
|
||||
const windows = require('./windows')
|
||||
|
||||
const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png')
|
||||
const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
|
||||
const PAUSE_ICON = path.join(config.STATIC_PATH, 'PauseThumbnailBarButton.png')
|
||||
const NEXT_ICON = path.join(config.STATIC_PATH, 'NextTrackThumbnailBarButton.png')
|
||||
|
||||
// Array indices for each button
|
||||
const PREV = 0
|
||||
const PLAY_PAUSE = 1
|
||||
const NEXT = 2
|
||||
|
||||
let buttons = []
|
||||
|
||||
/**
|
||||
* Show the Windows thumbnail toolbar buttons.
|
||||
*/
|
||||
function enable () {
|
||||
update(false)
|
||||
buttons = [
|
||||
{
|
||||
tooltip: 'Previous Track',
|
||||
icon: PREV_ICON,
|
||||
click: () => windows.main.dispatch('previousTrack')
|
||||
},
|
||||
{
|
||||
tooltip: 'Pause',
|
||||
icon: PAUSE_ICON,
|
||||
click: () => windows.main.dispatch('playPause')
|
||||
},
|
||||
{
|
||||
tooltip: 'Next Track',
|
||||
icon: NEXT_ICON,
|
||||
click: () => windows.main.dispatch('nextTrack')
|
||||
}
|
||||
]
|
||||
update()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the Windows thumbnail toolbar buttons.
|
||||
*/
|
||||
function disable () {
|
||||
windows.main.win.setThumbarButtons([])
|
||||
buttons = []
|
||||
update()
|
||||
}
|
||||
|
||||
function onPlayerPause () {
|
||||
update(true)
|
||||
if (!isEnabled()) return
|
||||
buttons[PLAY_PAUSE].tooltip = 'Play'
|
||||
buttons[PLAY_PAUSE].icon = PLAY_ICON
|
||||
update()
|
||||
}
|
||||
|
||||
function onPlayerPlay () {
|
||||
update(false)
|
||||
if (!isEnabled()) return
|
||||
buttons[PLAY_PAUSE].tooltip = 'Pause'
|
||||
buttons[PLAY_PAUSE].icon = PAUSE_ICON
|
||||
update()
|
||||
}
|
||||
|
||||
function update (isPaused) {
|
||||
var icon = isPaused
|
||||
? 'PlayThumbnailBarButton.png'
|
||||
: 'PauseThumbnailBarButton.png'
|
||||
function onPlayerUpdate (state) {
|
||||
if (!isEnabled()) return
|
||||
buttons[PREV].flags = [ state.hasPrevious ? 'enabled' : 'disabled' ]
|
||||
buttons[NEXT].flags = [ state.hasNext ? 'enabled' : 'disabled' ]
|
||||
update()
|
||||
}
|
||||
|
||||
var buttons = [
|
||||
{
|
||||
tooltip: isPaused ? 'Play' : 'Pause',
|
||||
icon: path.join(config.STATIC_PATH, icon),
|
||||
click: () => windows.main.dispatch('playPause')
|
||||
}
|
||||
]
|
||||
function isEnabled () {
|
||||
return buttons.length > 0
|
||||
}
|
||||
|
||||
function update () {
|
||||
windows.main.win.setThumbarButtons(buttons)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ module.exports = {
|
||||
setWindowFocus
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
const app = electron.app
|
||||
|
||||
var config = require('../config')
|
||||
var windows = require('./windows')
|
||||
const config = require('../config')
|
||||
const windows = require('./windows')
|
||||
|
||||
var tray
|
||||
let tray
|
||||
|
||||
function init () {
|
||||
if (process.platform === 'linux') {
|
||||
@@ -49,7 +49,7 @@ function initWin32 () {
|
||||
* Check for libappindicator1 support before creating tray icon
|
||||
*/
|
||||
function checkLinuxTraySupport (cb) {
|
||||
var cp = require('child_process')
|
||||
const cp = require('child_process')
|
||||
|
||||
// Check that we're on Ubuntu (or another debian system) and that we have
|
||||
// libappindicator1. If WebTorrent was installed from the deb file, we should
|
||||
@@ -74,7 +74,7 @@ function createTray () {
|
||||
}
|
||||
|
||||
function updateTrayMenu () {
|
||||
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||
tray.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,17 @@ module.exports = {
|
||||
init
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
var get = require('simple-get')
|
||||
const electron = require('electron')
|
||||
const get = require('simple-get')
|
||||
|
||||
var config = require('../config')
|
||||
var log = require('./log')
|
||||
var windows = require('./windows')
|
||||
const config = require('../config')
|
||||
const log = require('./log')
|
||||
const windows = require('./windows')
|
||||
|
||||
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||
'?version=' + config.APP_VERSION +
|
||||
'&platform=' + process.platform
|
||||
'&platform=' + process.platform +
|
||||
'&sysarch=' + config.OS_SYSARCH
|
||||
|
||||
function init () {
|
||||
if (process.platform === 'linux') {
|
||||
|
||||
@@ -2,9 +2,9 @@ module.exports = {
|
||||
init
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
const app = electron.app
|
||||
|
||||
/**
|
||||
* Add a user task menu to the app icon on right-click. (Windows)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
var about = module.exports = {
|
||||
const about = module.exports = {
|
||||
init,
|
||||
win: null
|
||||
}
|
||||
|
||||
var config = require('../../config')
|
||||
var electron = require('electron')
|
||||
const config = require('../../config')
|
||||
const electron = require('electron')
|
||||
|
||||
function init () {
|
||||
if (about.win) {
|
||||
return about.win.show()
|
||||
}
|
||||
|
||||
var win = about.win = new electron.BrowserWindow({
|
||||
const win = about.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#ECECEC',
|
||||
center: true,
|
||||
fullscreen: false,
|
||||
@@ -32,7 +32,7 @@ function init () {
|
||||
// No menu on the About window
|
||||
win.setMenu(null)
|
||||
|
||||
win.webContents.once('did-finish-load', function () {
|
||||
win.once('ready-to-show', function () {
|
||||
win.show()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var main = module.exports = {
|
||||
const main = module.exports = {
|
||||
dispatch,
|
||||
hide,
|
||||
init,
|
||||
@@ -14,24 +14,23 @@ var main = module.exports = {
|
||||
win: null
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
|
||||
var app = electron.app
|
||||
const app = electron.app
|
||||
|
||||
var config = require('../../config')
|
||||
var log = require('../log')
|
||||
var menu = require('../menu')
|
||||
var tray = require('../tray')
|
||||
const config = require('../../config')
|
||||
const log = require('../log')
|
||||
const menu = require('../menu')
|
||||
|
||||
var HEADER_HEIGHT = 37
|
||||
var TORRENT_HEIGHT = 100
|
||||
|
||||
function init () {
|
||||
function init (state, options) {
|
||||
if (main.win) {
|
||||
return main.win.show()
|
||||
}
|
||||
var win = main.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
|
||||
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
|
||||
|
||||
const win = main.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#282828',
|
||||
darkTheme: true, // Forces dark theme (GTK+3)
|
||||
icon: getIconPath(), // Window icon (Windows, Linux)
|
||||
minWidth: config.WINDOW_MIN_WIDTH,
|
||||
@@ -39,18 +38,33 @@ function init () {
|
||||
title: config.APP_WINDOW_TITLE,
|
||||
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
|
||||
useContentSize: true, // Specify web page size without OS chrome
|
||||
width: 500,
|
||||
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
||||
show: false,
|
||||
width: initialBounds.width,
|
||||
height: initialBounds.height,
|
||||
x: initialBounds.x,
|
||||
y: initialBounds.y
|
||||
})
|
||||
|
||||
win.loadURL(config.WINDOW_MAIN)
|
||||
|
||||
if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT)
|
||||
win.once('ready-to-show', () => {
|
||||
if (!options.hidden) win.show()
|
||||
})
|
||||
|
||||
if (win.setSheetOffset) {
|
||||
win.setSheetOffset(config.UI_HEADER_HEIGHT)
|
||||
}
|
||||
|
||||
win.webContents.on('dom-ready', function () {
|
||||
menu.onToggleFullScreen(main.win.isFullScreen())
|
||||
})
|
||||
|
||||
win.webContents.on('will-navigate', (e, url) => {
|
||||
// Prevent drag-and-drop from navigating the Electron window, which can happen
|
||||
// before our drag-and-drop handlers have been initialized.
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
win.on('blur', onWindowBlur)
|
||||
win.on('focus', onWindowFocus)
|
||||
|
||||
@@ -69,10 +83,23 @@ function init () {
|
||||
win.setMenuBarVisibility(true)
|
||||
})
|
||||
|
||||
win.on('move', function (e) {
|
||||
send('windowBoundsChanged', e.sender.getBounds())
|
||||
})
|
||||
|
||||
win.on('resize', function (e) {
|
||||
send('windowBoundsChanged', e.sender.getBounds())
|
||||
})
|
||||
|
||||
win.on('close', function (e) {
|
||||
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
||||
app.quit()
|
||||
} else if (!app.isQuitting) {
|
||||
if (process.platform !== 'darwin') {
|
||||
const tray = require('../tray')
|
||||
if (!tray.hasTray()) {
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!app.isQuitting) {
|
||||
e.preventDefault()
|
||||
hide()
|
||||
}
|
||||
@@ -85,7 +112,7 @@ function dispatch (...args) {
|
||||
|
||||
function hide () {
|
||||
if (!main.win) return
|
||||
main.win.send('dispatch', 'backToList')
|
||||
dispatch('backToList')
|
||||
main.win.hide()
|
||||
}
|
||||
|
||||
@@ -114,7 +141,7 @@ function setBounds (bounds, maximize) {
|
||||
}
|
||||
|
||||
// Maximize or minimize, if the second argument is present
|
||||
var willBeMaximized
|
||||
let willBeMaximized
|
||||
if (maximize === true) {
|
||||
if (!main.win.isMaximized()) {
|
||||
log('setBounds: maximizing')
|
||||
@@ -136,7 +163,7 @@ function setBounds (bounds, maximize) {
|
||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
||||
if (bounds.x === null && bounds.y === null) {
|
||||
// X and Y not specified? By default, center on current screen
|
||||
var scr = electron.screen.getDisplayMatching(main.win.getBounds())
|
||||
const scr = electron.screen.getDisplayMatching(main.win.getBounds())
|
||||
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||
@@ -211,12 +238,20 @@ function toggleFullScreen (flag) {
|
||||
|
||||
function onWindowBlur () {
|
||||
menu.setWindowFocus(false)
|
||||
tray.setWindowFocus(false)
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
const tray = require('../tray')
|
||||
tray.setWindowFocus(false)
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowFocus () {
|
||||
menu.setWindowFocus(true)
|
||||
tray.setWindowFocus(true)
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
const tray = require('../tray')
|
||||
tray.setWindowFocus(true)
|
||||
}
|
||||
}
|
||||
|
||||
function getIconPath () {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var webtorrent = module.exports = {
|
||||
const webtorrent = module.exports = {
|
||||
init,
|
||||
send,
|
||||
show,
|
||||
@@ -6,13 +6,12 @@ var webtorrent = module.exports = {
|
||||
win: null
|
||||
}
|
||||
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
|
||||
var config = require('../../config')
|
||||
var log = require('../log')
|
||||
const config = require('../../config')
|
||||
|
||||
function init () {
|
||||
var win = webtorrent.win = new electron.BrowserWindow({
|
||||
const win = webtorrent.win = new electron.BrowserWindow({
|
||||
backgroundColor: '#1E1E1E',
|
||||
center: true,
|
||||
fullscreen: false,
|
||||
@@ -52,7 +51,6 @@ function send (...args) {
|
||||
|
||||
function toggleDevTools () {
|
||||
if (!webtorrent.win) return
|
||||
log('toggleDevTools')
|
||||
if (webtorrent.win.webContents.isDevToolsOpened()) {
|
||||
webtorrent.win.webContents.closeDevTools()
|
||||
webtorrent.win.hide()
|
||||
|
||||
@@ -2,11 +2,14 @@ const React = require('react')
|
||||
|
||||
const {dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class Header extends React.Component {
|
||||
class Header extends React.Component {
|
||||
render () {
|
||||
var loc = this.props.state.location
|
||||
const loc = this.props.state.location
|
||||
return (
|
||||
<div key='header' className='header'>
|
||||
<div className='header'
|
||||
onMouseMove={dispatcher('mediaMouseMoved')}
|
||||
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
|
||||
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
|
||||
{this.getTitle()}
|
||||
<div className='nav left float-left'>
|
||||
<i
|
||||
@@ -31,12 +34,12 @@ module.exports = class Header extends React.Component {
|
||||
|
||||
getTitle () {
|
||||
if (process.platform !== 'darwin') return null
|
||||
var state = this.props.state
|
||||
const state = this.props.state
|
||||
return (<div className='title ellipsis'>{state.window.title}</div>)
|
||||
}
|
||||
|
||||
getAddButton () {
|
||||
var state = this.props.state
|
||||
const state = this.props.state
|
||||
if (state.location.url() !== 'home') return null
|
||||
return (
|
||||
<i
|
||||
@@ -48,3 +51,5 @@ module.exports = class Header extends React.Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Header
|
||||
34
src/renderer/components/heading.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const React = require('react')
|
||||
|
||||
const colors = require('material-ui/styles/colors')
|
||||
|
||||
class Heading extends React.Component {
|
||||
static get propTypes () {
|
||||
return {
|
||||
level: React.PropTypes.number
|
||||
}
|
||||
}
|
||||
|
||||
static get defaultProps () {
|
||||
return {
|
||||
level: 1
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const HeadingTag = 'h' + this.props.level
|
||||
const style = {
|
||||
color: colors.grey100,
|
||||
fontSize: 20,
|
||||
marginBottom: 15,
|
||||
marginTop: 30
|
||||
}
|
||||
return (
|
||||
<HeadingTag style={style}>
|
||||
{this.props.children}
|
||||
</HeadingTag>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Heading
|
||||
24
src/renderer/components/modal-ok-cancel.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const React = require('react')
|
||||
const FlatButton = require('material-ui/FlatButton').default
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
|
||||
module.exports = class ModalOKCancel extends React.Component {
|
||||
render () {
|
||||
const cancelStyle = { marginRight: 10, color: 'black' }
|
||||
const {cancelText, onCancel, okText, onOK} = this.props
|
||||
return (
|
||||
<div className='float-right'>
|
||||
<FlatButton
|
||||
className='control cancel'
|
||||
style={cancelStyle}
|
||||
label={cancelText}
|
||||
onClick={onCancel} />
|
||||
<RaisedButton
|
||||
className='control ok'
|
||||
primary
|
||||
label={okText}
|
||||
onClick={onOK} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
41
src/renderer/components/open-torrent-address-modal.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const React = require('react')
|
||||
const TextField = require('material-ui/TextField').default
|
||||
|
||||
const ModalOKCancel = require('./modal-ok-cancel')
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class OpenTorrentAddressModal extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className='open-torrent-address-modal'>
|
||||
<p><label>Enter torrent address or magnet link</label></p>
|
||||
<div>
|
||||
<TextField
|
||||
id='torrent-address-field'
|
||||
className='control'
|
||||
ref={(c) => { this.torrentURL = c }}
|
||||
fullWidth
|
||||
onKeyDown={handleKeyDown.bind(this)} />
|
||||
</div>
|
||||
<ModalOKCancel
|
||||
cancelText='CANCEL'
|
||||
onCancel={dispatcher('exitModal')}
|
||||
okText='OK'
|
||||
onOK={handleOK.bind(this)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.torrentURL.input.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown (e) {
|
||||
if (e.which === 13) handleOK.call(this) /* hit Enter to submit */
|
||||
}
|
||||
|
||||
function handleOK () {
|
||||
dispatch('exitModal')
|
||||
dispatch('addTorrent', this.torrentURL.input.value)
|
||||
}
|
||||
86
src/renderer/components/path-selector.js
Normal file
@@ -0,0 +1,86 @@
|
||||
const colors = require('material-ui/styles/colors')
|
||||
const electron = require('electron')
|
||||
const React = require('react')
|
||||
|
||||
const remote = electron.remote
|
||||
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
const TextField = require('material-ui/TextField').default
|
||||
|
||||
// Lets you pick a file or directory.
|
||||
// Uses the system Open File dialog.
|
||||
// You can't edit the text field directly.
|
||||
class PathSelector extends React.Component {
|
||||
static get propTypes () {
|
||||
return {
|
||||
className: React.PropTypes.string,
|
||||
dialog: React.PropTypes.object,
|
||||
displayValue: React.PropTypes.string,
|
||||
id: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.string
|
||||
}
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
const opts = Object.assign({
|
||||
defaultPath: this.props.value,
|
||||
properties: [ 'openFile', 'openDirectory' ]
|
||||
}, this.props.dialog)
|
||||
|
||||
remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
opts,
|
||||
(filenames) => {
|
||||
if (!Array.isArray(filenames)) return
|
||||
this.props.onChange && this.props.onChange(filenames[0])
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const id = this.props.title.replace(' ', '-').toLowerCase()
|
||||
const wrapperStyle = {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
width: '100%'
|
||||
}
|
||||
const labelStyle = {
|
||||
flex: '0 auto',
|
||||
marginRight: 10,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
const textareaStyle = {
|
||||
color: colors.grey50
|
||||
}
|
||||
const textFieldStyle = {
|
||||
flex: '1'
|
||||
}
|
||||
const text = this.props.displayValue || this.props.value
|
||||
const buttonStyle = {
|
||||
marginLeft: 10
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={this.props.className} style={wrapperStyle}>
|
||||
<div className='label' style={labelStyle}>
|
||||
{this.props.title}:
|
||||
</div>
|
||||
<TextField className='control' disabled id={id} value={text}
|
||||
inputStyle={textareaStyle} style={textFieldStyle} />
|
||||
<RaisedButton className='control' label='Change' onClick={this.handleClick}
|
||||
style={buttonStyle} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PathSelector
|
||||
@@ -1,22 +1,24 @@
|
||||
const React = require('react')
|
||||
|
||||
const ModalOKCancel = require('./modal-ok-cancel')
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class RemoveTorrentModal extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
var message = state.modal.deleteData
|
||||
const state = this.props.state
|
||||
const message = state.modal.deleteData
|
||||
? 'Are you sure you want to remove this torrent from the list and delete the data file?'
|
||||
: 'Are you sure you want to remove this torrent from the list?'
|
||||
var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove'
|
||||
const buttonText = state.modal.deleteData ? 'REMOVE DATA' : 'REMOVE'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p><strong>{message}</strong></p>
|
||||
<p className='float-right'>
|
||||
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
|
||||
<button className='button button-raised' onClick={handleRemove}>{buttonText}</button>
|
||||
</p>
|
||||
<ModalOKCancel
|
||||
cancelText='CANCEL'
|
||||
onCancel={dispatcher('exitModal')}
|
||||
okText={buttonText}
|
||||
onOK={handleRemove} />
|
||||
</div>
|
||||
)
|
||||
|
||||
53
src/renderer/components/show-more.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const React = require('react')
|
||||
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
|
||||
class ShowMore extends React.Component {
|
||||
static get propTypes () {
|
||||
return {
|
||||
defaultExpanded: React.PropTypes.bool,
|
||||
hideLabel: React.PropTypes.string,
|
||||
showLabel: React.PropTypes.string
|
||||
}
|
||||
}
|
||||
|
||||
static get defaultProps () {
|
||||
return {
|
||||
hideLabel: 'Hide more...',
|
||||
showLabel: 'Show more...'
|
||||
}
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: !!this.props.defaultExpanded
|
||||
}
|
||||
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
this.setState({
|
||||
expanded: !this.state.expanded
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const label = this.state.expanded
|
||||
? this.props.hideLabel
|
||||
: this.props.showLabel
|
||||
return (
|
||||
<div className='show-more' style={this.props.style}>
|
||||
{this.state.expanded ? this.props.children : null}
|
||||
<RaisedButton
|
||||
className='control'
|
||||
onClick={this.handleClick}
|
||||
label={label} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ShowMore
|
||||
44
src/renderer/components/unsupported-media-modal.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const React = require('react')
|
||||
const electron = require('electron')
|
||||
|
||||
const ModalOKCancel = require('./modal-ok-cancel')
|
||||
const {dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class UnsupportedMediaModal extends React.Component {
|
||||
render () {
|
||||
const state = this.props.state
|
||||
const err = state.modal.error
|
||||
const message = (err && err.getMessage)
|
||||
? err.getMessage()
|
||||
: err
|
||||
const onAction = state.modal.externalPlayerInstalled
|
||||
? dispatcher('openExternalPlayer')
|
||||
: () => this.onInstall()
|
||||
const actionText = state.modal.externalPlayerInstalled
|
||||
? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase()
|
||||
: 'INSTALL VLC'
|
||||
const errorMessage = state.modal.externalPlayerNotFound
|
||||
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
||||
: ''
|
||||
return (
|
||||
<div>
|
||||
<p><strong>Sorry, we can't play that file.</strong></p>
|
||||
<p>{message}</p>
|
||||
<ModalOKCancel
|
||||
cancelText='CANCEL'
|
||||
onCancel={dispatcher('backToList')}
|
||||
okText={actionText}
|
||||
onOK={onAction} />
|
||||
<p className='error-text'>{errorMessage}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onInstall () {
|
||||
electron.shell.openExternal('http://www.videolan.org/vlc/')
|
||||
|
||||
// TODO: dcposch send a dispatch rather than modifying state directly
|
||||
const state = this.props.state
|
||||
state.modal.externalPlayerInstalled = true // Assume they'll install it successfully
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
const React = require('react')
|
||||
const electron = require('electron')
|
||||
|
||||
const ModalOKCancel = require('./modal-ok-cancel')
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class UpdateAvailableModal extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
const state = this.props.state
|
||||
return (
|
||||
<div className='update-available-modal'>
|
||||
<p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p>
|
||||
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
|
||||
<p className='float-right'>
|
||||
<button className='button button-flat' onClick={handleSkip}>Skip This Release</button>
|
||||
<button className='button button-raised' onClick={handleShow}>Show Download Page</button>
|
||||
<p>
|
||||
We have an auto-updater for Windows and Mac.
|
||||
We don't have one for Linux yet, so you'll have to download the new version manually.
|
||||
</p>
|
||||
<ModalOKCancel
|
||||
cancelText='SKIP THIS RELEASE'
|
||||
onCancel={handleSkip}
|
||||
okText='SHOW DOWNLOAD PAGE'
|
||||
onOK={handleShow} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ const electron = require('electron')
|
||||
|
||||
const ipcRenderer = electron.ipcRenderer
|
||||
|
||||
const Playlist = require('../lib/playlist')
|
||||
|
||||
// Controls local play back: the <video>/<audio> tag and VLC
|
||||
// Does not control remote casting (Chromecast etc)
|
||||
module.exports = class MediaController {
|
||||
@@ -18,7 +20,7 @@ module.exports = class MediaController {
|
||||
}
|
||||
|
||||
mediaError (error) {
|
||||
var state = this.state
|
||||
const state = this.state
|
||||
if (state.location.url() === 'player') {
|
||||
state.playing.result = 'error'
|
||||
state.playing.location = 'error'
|
||||
@@ -42,14 +44,34 @@ module.exports = class MediaController {
|
||||
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||
}
|
||||
|
||||
controlsMouseEnter () {
|
||||
this.state.playing.mouseInControls = true
|
||||
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||
}
|
||||
|
||||
controlsMouseLeave () {
|
||||
this.state.playing.mouseInControls = false
|
||||
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||
}
|
||||
|
||||
openExternalPlayer () {
|
||||
var state = this.state
|
||||
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title)
|
||||
const state = this.state
|
||||
state.playing.location = 'external'
|
||||
|
||||
let open = function () {
|
||||
const mediaURL = Playlist.getCurrentLocalURL(state)
|
||||
ipcRenderer.send('openExternalPlayer',
|
||||
state.saved.prefs.externalPlayerPath,
|
||||
mediaURL,
|
||||
state.window.title)
|
||||
}
|
||||
|
||||
if (state.server != null) open()
|
||||
else ipcRenderer.once('wt-server-running', open)
|
||||
}
|
||||
|
||||
externalPlayerNotFound () {
|
||||
var modal = this.state.modal
|
||||
const modal = this.state.modal
|
||||
if (modal && modal.id === 'unsupported-media-modal') {
|
||||
modal.externalPlayerNotFound = true
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ const path = require('path')
|
||||
const Cast = require('../lib/cast')
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const telemetry = require('../lib/telemetry')
|
||||
const errors = require('../lib/errors')
|
||||
const {UnplayableFileError, UnplayableTorrentError} = require('../lib/errors')
|
||||
const sound = require('../lib/sound')
|
||||
const TorrentPlayer = require('../lib/torrent-player')
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const Playlist = require('../lib/playlist')
|
||||
const State = require('../lib/state')
|
||||
|
||||
const ipcRenderer = electron.ipcRenderer
|
||||
@@ -24,24 +25,43 @@ module.exports = class PlaybackController {
|
||||
// Play a file in a torrent.
|
||||
// * Start torrenting, if necessary
|
||||
// * Stream, if not already fully downloaded
|
||||
// * If no file index is provided, pick the default file to play
|
||||
// * If no file index is provided, restore the most recently viewed file or autoplay the first
|
||||
playFile (infoHash, index /* optional */) {
|
||||
this.state.location.go({
|
||||
url: 'player',
|
||||
setup: (cb) => {
|
||||
this.play()
|
||||
this.openPlayer(infoHash, index, cb)
|
||||
},
|
||||
destroy: () => this.closePlayer()
|
||||
}, (err) => {
|
||||
if (err) dispatch('error', err)
|
||||
})
|
||||
const state = this.state
|
||||
if (state.location.url() === 'player') {
|
||||
this.updatePlayer(infoHash, index, false, (err) => {
|
||||
if (err) dispatch('error', err)
|
||||
else this.play()
|
||||
})
|
||||
} else {
|
||||
let initialized = false
|
||||
state.location.go({
|
||||
url: 'player',
|
||||
setup: (cb) => {
|
||||
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||
|
||||
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
|
||||
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
|
||||
if (index === undefined) return cb(new UnplayableTorrentError())
|
||||
|
||||
initialized = true
|
||||
|
||||
this.openPlayer(infoHash, index, (err) => {
|
||||
if (!err) this.play()
|
||||
cb(err)
|
||||
})
|
||||
},
|
||||
destroy: () => this.closePlayer()
|
||||
}, (err) => {
|
||||
if (err) dispatch('error', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Show a file in the OS, eg in Finder on a Mac
|
||||
// Open a file in OS default app.
|
||||
openItem (infoHash, index) {
|
||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
var filePath = path.join(
|
||||
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
const filePath = path.join(
|
||||
torrentSummary.path,
|
||||
torrentSummary.files[index].path)
|
||||
ipcRenderer.send('openItem', filePath)
|
||||
@@ -49,12 +69,12 @@ module.exports = class PlaybackController {
|
||||
|
||||
// Toggle (play or pause) the currently playing media
|
||||
playPause () {
|
||||
var state = this.state
|
||||
const state = this.state
|
||||
if (state.location.url() !== 'player') return
|
||||
|
||||
// force rerendering if window is hidden,
|
||||
// in order to bypass `raf` and play/pause media immediately
|
||||
var mediaTag = document.querySelector('video,audio')
|
||||
const mediaTag = document.querySelector('video,audio')
|
||||
if (!state.window.isVisible && mediaTag) {
|
||||
if (state.playing.isPaused) mediaTag.play()
|
||||
else mediaTag.pause()
|
||||
@@ -64,9 +84,33 @@ module.exports = class PlaybackController {
|
||||
else this.pause()
|
||||
}
|
||||
|
||||
// Play next file in list (if any)
|
||||
nextTrack () {
|
||||
const state = this.state
|
||||
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
|
||||
this.updatePlayer(
|
||||
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
||||
if (err) dispatch('error', err)
|
||||
else this.play()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Play previous track in list (if any)
|
||||
previousTrack () {
|
||||
const state = this.state
|
||||
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
|
||||
this.updatePlayer(
|
||||
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
||||
if (err) dispatch('error', err)
|
||||
else this.play()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Play (unpause) the current media
|
||||
play () {
|
||||
var state = this.state
|
||||
const state = this.state
|
||||
if (!state.playing.isPaused) return
|
||||
state.playing.isPaused = false
|
||||
if (isCasting(state)) {
|
||||
@@ -77,7 +121,7 @@ module.exports = class PlaybackController {
|
||||
|
||||
// Pause the currently playing media
|
||||
pause () {
|
||||
var state = this.state
|
||||
const state = this.state
|
||||
if (state.playing.isPaused) return
|
||||
state.playing.isPaused = true
|
||||
if (isCasting(state)) {
|
||||
@@ -106,25 +150,23 @@ module.exports = class PlaybackController {
|
||||
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
|
||||
// until -16 (fast rewind)
|
||||
changePlaybackRate (direction) {
|
||||
var state = this.state
|
||||
var rate = state.playing.playbackRate
|
||||
if (direction > 0 && rate >= 0.25 && rate < 2) {
|
||||
const state = this.state
|
||||
let rate = state.playing.playbackRate
|
||||
if (direction > 0 && rate < 2) {
|
||||
rate += 0.25
|
||||
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
||||
rate -= 0.25
|
||||
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
|
||||
rate = -1
|
||||
} else if (direction > 0 && rate === -1) {
|
||||
rate = 0.25
|
||||
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
|
||||
} else if (direction > 0 && rate >= 1 && rate < 16) {
|
||||
rate *= 2
|
||||
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
|
||||
} else if (direction < 0 && rate > 1 && rate <= 16) {
|
||||
rate /= 2
|
||||
}
|
||||
state.playing.playbackRate = rate
|
||||
if (isCasting(state) && !Cast.setRate(rate)) {
|
||||
state.playing.playbackRate = 1
|
||||
}
|
||||
// Wait a bit before we hide the controls and header again
|
||||
state.playing.mouseStationarySince = new Date().getTime()
|
||||
}
|
||||
|
||||
// Change the volume, in range [0, 1], by some amount
|
||||
@@ -139,7 +181,7 @@ module.exports = class PlaybackController {
|
||||
// check if its in [0.0 - 1.0] range
|
||||
volume = Math.max(0, Math.min(1, volume))
|
||||
|
||||
var state = this.state
|
||||
const state = this.state
|
||||
if (isCasting(state)) {
|
||||
Cast.setVolume(volume)
|
||||
} else {
|
||||
@@ -153,12 +195,8 @@ module.exports = class PlaybackController {
|
||||
// * The video is paused
|
||||
// * The video is playing remotely on Chromecast or Airplay
|
||||
showOrHidePlayerControls () {
|
||||
var state = this.state
|
||||
var hideControls = state.location.url() === 'player' &&
|
||||
state.playing.mouseStationarySince !== 0 &&
|
||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||
!state.playing.isPaused &&
|
||||
state.playing.location === 'local'
|
||||
const state = this.state
|
||||
const hideControls = state.shouldHidePlayerControls()
|
||||
|
||||
if (hideControls !== state.playing.hideControls) {
|
||||
state.playing.hideControls = hideControls
|
||||
@@ -169,56 +207,65 @@ module.exports = class PlaybackController {
|
||||
|
||||
// Opens the video player to a specific torrent
|
||||
openPlayer (infoHash, index, cb) {
|
||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
const state = this.state
|
||||
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||
|
||||
// automatically choose which file in the torrent to play, if necessary
|
||||
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
|
||||
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
|
||||
if (index === undefined) return cb(new errors.UnplayableError())
|
||||
state.playing.infoHash = torrentSummary.infoHash
|
||||
|
||||
// update UI to show pending playback
|
||||
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
||||
// TODO: remove torrentSummary.playStatus
|
||||
torrentSummary.playStatus = 'requested'
|
||||
this.update()
|
||||
sound.play('PLAY')
|
||||
|
||||
var timeout = setTimeout(() => {
|
||||
telemetry.logPlayAttempt('timeout')
|
||||
// TODO: remove torrentSummary.playStatus
|
||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||
sound.play('ERROR')
|
||||
cb(new Error('Playback timed out. Try again.'))
|
||||
this.update()
|
||||
}, 10000) /* give it a few seconds */
|
||||
this.startServer(torrentSummary)
|
||||
ipcRenderer.send('onPlayerOpen')
|
||||
this.updatePlayer(infoHash, index, true, cb)
|
||||
}
|
||||
|
||||
// Starts WebTorrent server for media streaming
|
||||
startServer (torrentSummary) {
|
||||
if (torrentSummary.status === 'paused') {
|
||||
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
||||
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
||||
() => onTorrentReady())
|
||||
} else {
|
||||
this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
|
||||
onTorrentReady()
|
||||
}
|
||||
|
||||
function onTorrentReady () {
|
||||
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
||||
var fileSummary = torrentSummary.files[index]
|
||||
// Called each time the current file changes
|
||||
updatePlayer (infoHash, index, resume, cb) {
|
||||
const state = this.state
|
||||
|
||||
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||
const fileSummary = torrentSummary.files[index]
|
||||
|
||||
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
||||
torrentSummary.mostRecentFileIndex = undefined
|
||||
return cb(new UnplayableFileError())
|
||||
}
|
||||
|
||||
torrentSummary.mostRecentFileIndex = index
|
||||
|
||||
// update state
|
||||
var state = this.state
|
||||
state.playing.infoHash = torrentSummary.infoHash
|
||||
state.playing.infoHash = infoHash
|
||||
state.playing.fileIndex = index
|
||||
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
||||
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||
: 'other'
|
||||
|
||||
// pick up where we left off
|
||||
if (fileSummary.currentTime) {
|
||||
var fraction = fileSummary.currentTime / fileSummary.duration
|
||||
var secondsLeft = fileSummary.duration - fileSummary.currentTime
|
||||
let jumpToTime = 0
|
||||
if (resume && fileSummary.currentTime) {
|
||||
const fraction = fileSummary.currentTime / fileSummary.duration
|
||||
const secondsLeft = fileSummary.duration - fileSummary.currentTime
|
||||
if (fraction < 0.9 && secondsLeft > 10) {
|
||||
state.playing.jumpToTime = fileSummary.currentTime
|
||||
jumpToTime = fileSummary.currentTime
|
||||
}
|
||||
}
|
||||
state.playing.jumpToTime = jumpToTime
|
||||
|
||||
// if it's audio, parse out the metadata (artist, title, etc)
|
||||
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
||||
@@ -233,41 +280,28 @@ module.exports = class PlaybackController {
|
||||
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
||||
}
|
||||
|
||||
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
||||
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
|
||||
clearTimeout(timeout)
|
||||
state.window.title = fileSummary.name
|
||||
|
||||
// if we timed out (user clicked play a long time ago), don't autoplay
|
||||
var timedOut = torrentSummary.playStatus === 'timeout'
|
||||
delete torrentSummary.playStatus
|
||||
if (timedOut) {
|
||||
ipcRenderer.send('wt-stop-server')
|
||||
return this.update()
|
||||
}
|
||||
|
||||
state.window.title = torrentSummary.files[state.playing.fileIndex].name
|
||||
|
||||
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
||||
if (this.state.saved.prefs.openExternalPlayer) {
|
||||
dispatch('openExternalPlayer')
|
||||
this.update()
|
||||
cb()
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, play the video
|
||||
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
||||
if (this.state.saved.prefs.openExternalPlayer) {
|
||||
dispatch('openExternalPlayer')
|
||||
this.update()
|
||||
|
||||
ipcRenderer.send('onPlayerOpen')
|
||||
cb()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, play the video
|
||||
this.update()
|
||||
|
||||
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
|
||||
cb()
|
||||
}
|
||||
|
||||
closePlayer () {
|
||||
console.log('closePlayer')
|
||||
|
||||
// Quit any external players, like Chromecast/Airplay/etc or VLC
|
||||
var state = this.state
|
||||
const state = this.state
|
||||
if (isCasting(state)) {
|
||||
Cast.stop()
|
||||
}
|
||||
@@ -279,10 +313,10 @@ module.exports = class PlaybackController {
|
||||
state.previousVolume = state.playing.volume
|
||||
|
||||
// Telemetry: track what happens after the user clicks play
|
||||
var result = state.playing.result // 'success' or 'error'
|
||||
const result = state.playing.result // 'success' or 'error'
|
||||
if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed
|
||||
else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc
|
||||
else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame
|
||||
else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user gave up waiting
|
||||
else console.error('Unknown state.playing.result', state.playing.result)
|
||||
|
||||
// Reset the window contents back to the home screen
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const State = require('../lib/state')
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const ipcRenderer = require('electron').ipcRenderer
|
||||
|
||||
@@ -11,13 +10,15 @@ module.exports = class PrefsController {
|
||||
|
||||
// Goes to the Preferences screen
|
||||
show () {
|
||||
var state = this.state
|
||||
const state = this.state
|
||||
state.location.go({
|
||||
url: 'preferences',
|
||||
setup: function (cb) {
|
||||
// initialize preferences
|
||||
state.window.title = 'Preferences'
|
||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||
state.unsaved = Object.assign(state.unsaved || {}, {
|
||||
prefs: Object.assign({}, state.saved.prefs)
|
||||
})
|
||||
ipcRenderer.send('setAllowNav', false)
|
||||
cb()
|
||||
},
|
||||
@@ -32,25 +33,29 @@ module.exports = class PrefsController {
|
||||
// For example: updatePreferences('foo.bar', 'baz')
|
||||
// Call save() to save to config.json
|
||||
update (property, value) {
|
||||
var path = property.split('.')
|
||||
var key = this.state.unsaved.prefs
|
||||
for (var i = 0; i < path.length - 1; i++) {
|
||||
if (typeof key[path[i]] === 'undefined') {
|
||||
key[path[i]] = {}
|
||||
const path = property.split('.')
|
||||
let obj = this.state.unsaved.prefs
|
||||
let i
|
||||
for (i = 0; i < path.length - 1; i++) {
|
||||
if (typeof obj[path[i]] === 'undefined') {
|
||||
obj[path[i]] = {}
|
||||
}
|
||||
key = key[path[i]]
|
||||
obj = obj[path[i]]
|
||||
}
|
||||
key[path[i]] = value
|
||||
obj[path[i]] = value
|
||||
}
|
||||
|
||||
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||
save () {
|
||||
var state = this.state
|
||||
const state = this.state
|
||||
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
|
||||
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
|
||||
}
|
||||
if (state.unsaved.prefs.startup !== state.saved.prefs.startup) {
|
||||
ipcRenderer.send('setStartup', state.unsaved.prefs.startup)
|
||||
}
|
||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||
State.save(state)
|
||||
dispatch('stateSaveImmediate')
|
||||
dispatch('checkDownloadPath')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const electron = require('electron')
|
||||
const fs = require('fs-extra')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const parallel = require('run-parallel')
|
||||
|
||||
@@ -28,36 +28,35 @@ module.exports = class SubtitlesController {
|
||||
}
|
||||
|
||||
toggleSubtitlesMenu () {
|
||||
var subtitles = this.state.playing.subtitles
|
||||
const subtitles = this.state.playing.subtitles
|
||||
subtitles.showMenu = !subtitles.showMenu
|
||||
}
|
||||
|
||||
addSubtitles (files, autoSelect) {
|
||||
var state = this.state
|
||||
// Subtitles are only supported when playing video files
|
||||
if (state.playing.type !== 'video') return
|
||||
if (this.state.playing.type !== 'video') return
|
||||
if (files.length === 0) return
|
||||
var subtitles = state.playing.subtitles
|
||||
const subtitles = this.state.playing.subtitles
|
||||
|
||||
// Read the files concurrently, then add all resulting subtitle tracks
|
||||
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||
parallel(tasks, function (err, tracks) {
|
||||
if (err) return dispatch('error', err)
|
||||
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
// No dupes allowed
|
||||
var track = tracks[i]
|
||||
var trackIndex = state.playing.subtitles.tracks
|
||||
.findIndex((t) => track.filePath === t.filePath)
|
||||
const track = tracks[i]
|
||||
let trackIndex = subtitles.tracks.findIndex((t) =>
|
||||
track.filePath === t.filePath)
|
||||
|
||||
// Add the track
|
||||
if (trackIndex === -1) {
|
||||
trackIndex = state.playing.subtitles.tracks.push(track) - 1
|
||||
trackIndex = subtitles.tracks.push(track) - 1
|
||||
}
|
||||
|
||||
// If we're auto-selecting a track, try to find one in the user's language
|
||||
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||
state.playing.subtitles.selectedIndex = trackIndex
|
||||
subtitles.selectedIndex = trackIndex
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,46 +67,46 @@ module.exports = class SubtitlesController {
|
||||
|
||||
checkForSubtitles () {
|
||||
if (this.state.playing.type !== 'video') return
|
||||
var torrentSummary = this.state.getPlayingTorrentSummary()
|
||||
const torrentSummary = this.state.getPlayingTorrentSummary()
|
||||
if (!torrentSummary || !torrentSummary.progress) return
|
||||
|
||||
torrentSummary.progress.files.forEach((fp, ix) => {
|
||||
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
|
||||
var file = torrentSummary.files[ix]
|
||||
const file = torrentSummary.files[ix]
|
||||
if (!this.isSubtitle(file.name)) return
|
||||
var filePath = path.join(torrentSummary.path, file.path)
|
||||
const filePath = path.join(torrentSummary.path, file.path)
|
||||
this.addSubtitles([filePath], false)
|
||||
})
|
||||
}
|
||||
|
||||
isSubtitle (file) {
|
||||
var name = typeof file === 'string' ? file : file.name
|
||||
var ext = path.extname(name).toLowerCase()
|
||||
const name = typeof file === 'string' ? file : file.name
|
||||
const ext = path.extname(name).toLowerCase()
|
||||
return ext === '.srt' || ext === '.vtt'
|
||||
}
|
||||
}
|
||||
|
||||
function loadSubtitle (file, cb) {
|
||||
// Lazy load to keep startup fast
|
||||
var concat = require('simple-concat')
|
||||
var LanguageDetect = require('languagedetect')
|
||||
var srtToVtt = require('srt-to-vtt')
|
||||
const concat = require('simple-concat')
|
||||
const LanguageDetect = require('languagedetect')
|
||||
const srtToVtt = require('srt-to-vtt')
|
||||
|
||||
// Read the .SRT or .VTT file, parse it, add subtitle track
|
||||
var filePath = file.path || file
|
||||
const filePath = file.path || file
|
||||
|
||||
var vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
||||
const vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
||||
|
||||
concat(vttStream, function (err, buf) {
|
||||
if (err) return dispatch('error', 'Can\'t parse subtitles file.')
|
||||
|
||||
// Detect what language the subtitles are in
|
||||
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
||||
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
||||
const vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
||||
let langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
||||
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
||||
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
||||
|
||||
var track = {
|
||||
const track = {
|
||||
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
||||
language: langDetected,
|
||||
label: langDetected,
|
||||
@@ -121,18 +120,18 @@ function loadSubtitle (file, cb) {
|
||||
// Checks whether a language name like 'English' or 'German' matches the system
|
||||
// language, aka the current locale
|
||||
function isSystemLanguage (language) {
|
||||
var iso639 = require('iso-639-1')
|
||||
var osLangISO = window.navigator.language.split('-')[0] // eg 'en'
|
||||
var langIso = iso639.getCode(language) // eg 'de' if language is 'German'
|
||||
const iso639 = require('iso-639-1')
|
||||
const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
|
||||
const langIso = iso639.getCode(language) // eg 'de' if language is 'German'
|
||||
return langIso === osLangISO
|
||||
}
|
||||
|
||||
// Make sure we don't have two subtitle tracks with the same label
|
||||
// Labels each track by language, eg 'German', 'English', 'English 2', ...
|
||||
function relabelSubtitles (subtitles) {
|
||||
var counts = {}
|
||||
const counts = {}
|
||||
subtitles.tracks.forEach(function (track) {
|
||||
var lang = track.language
|
||||
const lang = track.language
|
||||
counts[lang] = (counts[lang] || 0) + 1
|
||||
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ const path = require('path')
|
||||
const ipcRenderer = require('electron').ipcRenderer
|
||||
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const TorrentPlayer = require('../lib/torrent-player')
|
||||
const sound = require('../lib/sound')
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
@@ -12,12 +11,12 @@ module.exports = class TorrentController {
|
||||
}
|
||||
|
||||
torrentInfoHash (torrentKey, infoHash) {
|
||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
let torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
console.log('got infohash for %s torrent %s',
|
||||
torrentSummary ? 'existing' : 'new', torrentKey)
|
||||
|
||||
if (!torrentSummary) {
|
||||
var torrents = this.state.saved.torrents
|
||||
const torrents = this.state.saved.torrents
|
||||
|
||||
// Check if an existing (non-active) torrent has the same info hash
|
||||
if (torrents.find((t) => t.infoHash === infoHash)) {
|
||||
@@ -49,7 +48,7 @@ module.exports = class TorrentController {
|
||||
}
|
||||
dispatch('error', message)
|
||||
|
||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
if (torrentSummary) {
|
||||
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
|
||||
torrentSummary.status = 'paused'
|
||||
@@ -59,21 +58,20 @@ module.exports = class TorrentController {
|
||||
|
||||
torrentMetadata (torrentKey, torrentInfo) {
|
||||
// Summarize torrent
|
||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
torrentSummary.status = 'downloading'
|
||||
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
||||
torrentSummary.path = torrentInfo.path
|
||||
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
||||
// For now, check whether torrentSummary.files has already been set:
|
||||
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
||||
const hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
||||
if (!hasDetailedFileInfo) {
|
||||
torrentSummary.files = torrentInfo.files
|
||||
}
|
||||
if (!torrentSummary.selections) {
|
||||
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||
}
|
||||
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
|
||||
dispatch('update')
|
||||
|
||||
// Save the .torrent file, if it hasn't been saved already
|
||||
@@ -85,7 +83,7 @@ module.exports = class TorrentController {
|
||||
|
||||
torrentDone (torrentKey, torrentInfo) {
|
||||
// Update the torrent summary
|
||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
torrentSummary.status = 'seeding'
|
||||
|
||||
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
|
||||
@@ -102,22 +100,18 @@ module.exports = class TorrentController {
|
||||
}
|
||||
|
||||
torrentProgress (progressInfo) {
|
||||
// Overall progress across all active torrents, 0 to 1
|
||||
var progress = progressInfo.progress
|
||||
var hasActiveTorrents = progressInfo.hasActiveTorrents
|
||||
|
||||
// Overall progress across all active torrents, 0 to 1, or -1 to hide the progress bar
|
||||
// Hide progress bar when client has no torrents, or progress is 100%
|
||||
// TODO: isn't this equivalent to: if (progress === 1) ?
|
||||
if (!hasActiveTorrents || progress === 1) {
|
||||
progress = -1
|
||||
}
|
||||
const progress = (!progressInfo.hasActiveTorrents || progressInfo.progress === 1)
|
||||
? -1
|
||||
: progressInfo.progress
|
||||
|
||||
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
||||
this.state.dock.progress = progress
|
||||
|
||||
// Update progress for each individual torrent
|
||||
progressInfo.torrents.forEach((p) => {
|
||||
var torrentSummary = this.getTorrentSummary(p.torrentKey)
|
||||
const torrentSummary = this.getTorrentSummary(p.torrentKey)
|
||||
if (!torrentSummary) {
|
||||
console.log('warning: got progress for missing torrent %s', p.torrentKey)
|
||||
return
|
||||
@@ -132,27 +126,28 @@ module.exports = class TorrentController {
|
||||
}
|
||||
|
||||
torrentFileModtimes (torrentKey, fileModtimes) {
|
||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
if (!torrentSummary) throw new Error('Not saving modtimes for deleted torrent ' + torrentKey)
|
||||
torrentSummary.fileModtimes = fileModtimes
|
||||
dispatch('saveStateThrottled')
|
||||
dispatch('stateSave')
|
||||
}
|
||||
|
||||
torrentFileSaved (torrentKey, torrentFileName) {
|
||||
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
torrentSummary.torrentFileName = torrentFileName
|
||||
dispatch('saveStateThrottled')
|
||||
dispatch('stateSave')
|
||||
}
|
||||
|
||||
torrentPosterSaved (torrentKey, posterFileName) {
|
||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||
torrentSummary.posterFileName = posterFileName
|
||||
dispatch('saveStateThrottled')
|
||||
dispatch('stateSave')
|
||||
}
|
||||
|
||||
torrentAudioMetadata (infoHash, index, info) {
|
||||
var torrentSummary = this.getTorrentSummary(infoHash)
|
||||
var fileSummary = torrentSummary.files[index]
|
||||
const torrentSummary = this.getTorrentSummary(infoHash)
|
||||
const fileSummary = torrentSummary.files[index]
|
||||
fileSummary.audioInfo = info
|
||||
dispatch('update')
|
||||
}
|
||||
@@ -169,7 +164,7 @@ module.exports = class TorrentController {
|
||||
}
|
||||
|
||||
function getTorrentPath (torrentSummary) {
|
||||
var itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
let itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
if (torrentSummary.files.length > 1) {
|
||||
itemPath = path.dirname(itemPath)
|
||||
}
|
||||
@@ -177,14 +172,15 @@ function getTorrentPath (torrentSummary) {
|
||||
}
|
||||
|
||||
function showDoneNotification (torrent) {
|
||||
var notif = new window.Notification('Download Complete', {
|
||||
const notif = new window.Notification('Download Complete', {
|
||||
body: torrent.name,
|
||||
silent: true
|
||||
})
|
||||
|
||||
notif.onClick = function () {
|
||||
notif.onclick = function () {
|
||||
ipcRenderer.send('show')
|
||||
}
|
||||
|
||||
sound.play('DONE')
|
||||
// Only play notification sound if player is inactive
|
||||
if (this.state.playing.isPaused) sound.play('DONE')
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ const path = require('path')
|
||||
const electron = require('electron')
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const State = require('../lib/state')
|
||||
const {TorrentKeyNotFoundError} = require('../lib/errors')
|
||||
const sound = require('../lib/sound')
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
|
||||
@@ -17,21 +17,26 @@ module.exports = class TorrentListController {
|
||||
this.state = state
|
||||
}
|
||||
|
||||
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
||||
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||
// Adds a torrent to the list, starts downloading/seeding.
|
||||
// TorrentID can be a magnet URI, infohash, or torrent file: https://git.io/vik9M
|
||||
addTorrent (torrentId) {
|
||||
if (torrentId.path) {
|
||||
// Use path string instead of W3C File object
|
||||
torrentId = torrentId.path
|
||||
}
|
||||
|
||||
// Trim extra spaces off pasted magnet links
|
||||
if (typeof torrentId === 'string') {
|
||||
torrentId = torrentId.trim()
|
||||
}
|
||||
|
||||
// Allow a instant.io link to be pasted
|
||||
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||
}
|
||||
|
||||
var torrentKey = this.state.nextTorrentKey++
|
||||
var path = this.state.saved.prefs.downloadPath
|
||||
const torrentKey = this.state.nextTorrentKey++
|
||||
const path = this.state.saved.prefs.downloadPath
|
||||
|
||||
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||
|
||||
@@ -64,25 +69,18 @@ module.exports = class TorrentListController {
|
||||
findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles))
|
||||
}
|
||||
|
||||
// Switches between the advanced and simple Create Torrent UI
|
||||
toggleCreateTorrentAdvanced () {
|
||||
var info = this.state.location.current()
|
||||
if (info.url !== 'create-torrent') return
|
||||
info.showAdvanced = !info.showAdvanced
|
||||
}
|
||||
|
||||
// Creates a new torrent and start seeeding
|
||||
createTorrent (options) {
|
||||
var state = this.state
|
||||
var torrentKey = state.nextTorrentKey++
|
||||
const state = this.state
|
||||
const torrentKey = state.nextTorrentKey++
|
||||
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||
state.location.cancel()
|
||||
}
|
||||
|
||||
// Starts downloading and/or seeding a given torrentSummary.
|
||||
startTorrentingSummary (torrentKey) {
|
||||
var s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||
if (!s) throw new Error('Missing key: ' + torrentKey)
|
||||
const s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||
if (!s) throw new TorrentKeyNotFoundError(torrentKey)
|
||||
|
||||
// New torrent: give it a path
|
||||
if (!s.path) {
|
||||
@@ -91,10 +89,16 @@ module.exports = class TorrentListController {
|
||||
return start()
|
||||
}
|
||||
|
||||
const fileOrFolder = TorrentSummary.getFileOrFolder(s)
|
||||
|
||||
// New torrent: metadata not yet received
|
||||
if (!fileOrFolder) return start()
|
||||
|
||||
// Existing torrent: check that the path is still there
|
||||
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
|
||||
fs.stat(fileOrFolder, function (err) {
|
||||
if (err) {
|
||||
s.error = 'path-missing'
|
||||
dispatch('backToList')
|
||||
return
|
||||
}
|
||||
start()
|
||||
@@ -103,7 +107,7 @@ module.exports = class TorrentListController {
|
||||
function start () {
|
||||
ipcRenderer.send('wt-start-torrenting',
|
||||
s.torrentKey,
|
||||
TorrentSummary.getTorrentID(s),
|
||||
TorrentSummary.getTorrentId(s),
|
||||
s.path,
|
||||
s.fileModtimes,
|
||||
s.selections)
|
||||
@@ -112,7 +116,7 @@ module.exports = class TorrentListController {
|
||||
|
||||
// TODO: use torrentKey, not infoHash
|
||||
toggleTorrent (infoHash) {
|
||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
if (torrentSummary.status === 'paused') {
|
||||
torrentSummary.status = 'new'
|
||||
this.startTorrentingSummary(torrentSummary.torrentKey)
|
||||
@@ -125,11 +129,13 @@ module.exports = class TorrentListController {
|
||||
}
|
||||
|
||||
toggleTorrentFile (infoHash, index) {
|
||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
||||
|
||||
// Let the WebTorrent process know to start or stop fetching that file
|
||||
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||
if (torrentSummary.status !== 'paused') {
|
||||
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||
}
|
||||
}
|
||||
|
||||
confirmDeleteTorrent (infoHash, deleteData) {
|
||||
@@ -144,24 +150,25 @@ module.exports = class TorrentListController {
|
||||
deleteTorrent (infoHash, deleteData) {
|
||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||
|
||||
var index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||
const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||
|
||||
if (index > -1) {
|
||||
var summary = this.state.saved.torrents[index]
|
||||
const summary = this.state.saved.torrents[index]
|
||||
|
||||
// remove torrent and poster file
|
||||
deleteFile(TorrentSummary.getTorrentPath(summary))
|
||||
deleteFile(TorrentSummary.getPosterPath(summary)) // TODO: will the css path hack affect windows?
|
||||
deleteFile(TorrentSummary.getPosterPath(summary))
|
||||
|
||||
// optionally delete the torrent data
|
||||
if (deleteData) moveItemToTrash(summary)
|
||||
|
||||
// remove torrent from saved list
|
||||
this.state.saved.torrents.splice(index, 1)
|
||||
State.saveThrottled(this.state)
|
||||
dispatch('stateSave')
|
||||
}
|
||||
|
||||
this.state.location.clearForward('player') // prevent user from going forward to a deleted torrent
|
||||
// prevent user from going forward to a deleted torrent
|
||||
this.state.location.clearForward('player')
|
||||
sound.play('DELETE')
|
||||
}
|
||||
|
||||
@@ -174,8 +181,8 @@ module.exports = class TorrentListController {
|
||||
}
|
||||
|
||||
openTorrentContextMenu (infoHash) {
|
||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
var menu = new electron.remote.Menu()
|
||||
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||
const menu = new electron.remote.Menu()
|
||||
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: 'Remove From List',
|
||||
@@ -213,22 +220,52 @@ module.exports = class TorrentListController {
|
||||
|
||||
menu.append(new electron.remote.MenuItem({
|
||||
label: 'Save Torrent File As...',
|
||||
click: () => saveTorrentFileAs(torrentSummary)
|
||||
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey)
|
||||
}))
|
||||
|
||||
menu.popup(electron.remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
// Takes a torrentSummary or torrentKey
|
||||
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
|
||||
saveTorrentFileAs (torrentKey) {
|
||||
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
|
||||
if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
|
||||
const downloadPath = this.state.saved.prefs.downloadPath
|
||||
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||
const win = electron.remote.getCurrentWindow()
|
||||
const opts = {
|
||||
title: 'Save Torrent File',
|
||||
defaultPath: path.join(downloadPath, newFileName),
|
||||
filters: [
|
||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
]
|
||||
}
|
||||
|
||||
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
|
||||
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
|
||||
if (!savePath) return // They clicked Cancel
|
||||
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||
if (err) return dispatch('error', err)
|
||||
fs.writeFile(savePath, torrentFile, function (err) {
|
||||
if (err) return dispatch('error', err)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively finds {name, path, size} for all files in a folder
|
||||
// Calls `cb` on success, calls `onError` on failure
|
||||
function findFilesRecursive (paths, cb) {
|
||||
if (paths.length > 1) {
|
||||
var numComplete = 0
|
||||
var ret = []
|
||||
let numComplete = 0
|
||||
let ret = []
|
||||
paths.forEach(function (path) {
|
||||
findFilesRecursive([path], function (fileObjs) {
|
||||
ret = ret.concat(fileObjs)
|
||||
ret.push(...fileObjs)
|
||||
if (++numComplete === paths.length) {
|
||||
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
|
||||
cb(ret)
|
||||
@@ -238,13 +275,13 @@ function findFilesRecursive (paths, cb) {
|
||||
return
|
||||
}
|
||||
|
||||
var fileOrFolder = paths[0]
|
||||
const fileOrFolder = paths[0]
|
||||
fs.stat(fileOrFolder, function (err, stat) {
|
||||
if (err) return dispatch('error', err)
|
||||
|
||||
// Files: return name, path, and size
|
||||
if (!stat.isDirectory()) {
|
||||
var filePath = fileOrFolder
|
||||
const filePath = fileOrFolder
|
||||
return cb([{
|
||||
name: path.basename(filePath),
|
||||
path: filePath,
|
||||
@@ -253,10 +290,10 @@ function findFilesRecursive (paths, cb) {
|
||||
}
|
||||
|
||||
// Folders: recurse, make a list of all the files
|
||||
var folderPath = fileOrFolder
|
||||
const folderPath = fileOrFolder
|
||||
fs.readdir(folderPath, function (err, fileNames) {
|
||||
if (err) return dispatch('error', err)
|
||||
var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
||||
const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
||||
findFilesRecursive(paths, cb)
|
||||
})
|
||||
})
|
||||
@@ -271,33 +308,10 @@ function deleteFile (path) {
|
||||
|
||||
// Delete all files in a torrent
|
||||
function moveItemToTrash (torrentSummary) {
|
||||
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
const filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
|
||||
}
|
||||
|
||||
function showItemInFolder (torrentSummary) {
|
||||
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
||||
}
|
||||
|
||||
function saveTorrentFileAs (torrentSummary) {
|
||||
var downloadPath = this.state.saved.prefs.downloadPath
|
||||
var newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||
var opts = {
|
||||
title: 'Save Torrent File',
|
||||
defaultPath: path.join(downloadPath, newFileName),
|
||||
filters: [
|
||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
]
|
||||
}
|
||||
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
||||
if (!savePath) return // They clicked Cancel
|
||||
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||
if (err) return dispatch('error', err)
|
||||
fs.writeFile(savePath, torrentFile, function (err) {
|
||||
if (err) return dispatch('error', err)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const State = require('../lib/state')
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
// Controls the UI checking for new versions of the app, prompting install
|
||||
module.exports = class UpdateController {
|
||||
@@ -8,7 +8,7 @@ module.exports = class UpdateController {
|
||||
|
||||
// Shows a modal saying that we have an update
|
||||
updateAvailable (version) {
|
||||
var skipped = this.state.saved.skippedVersions
|
||||
const skipped = this.state.saved.skippedVersions
|
||||
if (skipped && skipped.includes(version)) {
|
||||
console.log('new version skipped by user: v' + version)
|
||||
return
|
||||
@@ -18,9 +18,9 @@ module.exports = class UpdateController {
|
||||
|
||||
// Don't show the modal again until the next version
|
||||
skipVersion (version) {
|
||||
var skipped = this.state.saved.skippedVersions
|
||||
let skipped = this.state.saved.skippedVersions
|
||||
if (!skipped) skipped = this.state.saved.skippedVersions = []
|
||||
skipped.push(version)
|
||||
State.saveThrottled(this.state)
|
||||
dispatch('stateSave')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
module.exports = captureVideoFrame
|
||||
|
||||
function captureVideoFrame (video, format) {
|
||||
if (typeof video === 'string') {
|
||||
video = document.querySelector(video)
|
||||
}
|
||||
|
||||
if (video == null || video.nodeName !== 'VIDEO') {
|
||||
throw new Error('First argument must be a <video> element or selector')
|
||||
}
|
||||
|
||||
if (format == null) {
|
||||
format = 'png'
|
||||
}
|
||||
|
||||
if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
|
||||
throw new Error('Second argument must be one of "png", "jpg", or "webp"')
|
||||
}
|
||||
|
||||
var canvas = document.createElement('canvas')
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
|
||||
canvas.getContext('2d').drawImage(video, 0, 0)
|
||||
|
||||
var dataUri = canvas.toDataURL('image/' + format)
|
||||
var data = dataUri.split(',')[1]
|
||||
|
||||
return new Buffer(data, 'base64')
|
||||
}
|
||||
@@ -13,25 +13,35 @@ module.exports = {
|
||||
setRate
|
||||
}
|
||||
|
||||
// Lazy load these for a ~300ms improvement in startup time
|
||||
var airplayer, chromecasts, dlnacasts
|
||||
const config = require('../../config')
|
||||
const {CastingError} = require('./errors')
|
||||
|
||||
var config = require('../../config')
|
||||
// Lazy load these for a ~300ms improvement in startup time
|
||||
let airplayer, chromecasts, dlnacasts
|
||||
|
||||
// App state. Cast modifies state.playing and state.errors in response to events
|
||||
var state
|
||||
let state
|
||||
|
||||
// Callback to notify module users when state has changed
|
||||
var update
|
||||
let update
|
||||
|
||||
// setInterval() for updating cast status
|
||||
var statusInterval = null
|
||||
let statusInterval = null
|
||||
|
||||
// Start looking for cast devices on the local network
|
||||
function init (appState, callback) {
|
||||
state = appState
|
||||
update = callback
|
||||
|
||||
// Don't actually cast during integration tests
|
||||
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
|
||||
if (config.IS_TEST) {
|
||||
state.devices.chromecast = testPlayer('chromecast')
|
||||
state.devices.airplay = testPlayer('airplay')
|
||||
state.devices.dlna = testPlayer('dlna')
|
||||
return
|
||||
}
|
||||
|
||||
// Load modules, scan the network for devices
|
||||
airplayer = require('airplayer')()
|
||||
chromecasts = require('chromecasts')()
|
||||
@@ -57,9 +67,35 @@ function init (appState, callback) {
|
||||
})
|
||||
}
|
||||
|
||||
// integration test player implementation
|
||||
function testPlayer (type) {
|
||||
return {
|
||||
getDevices,
|
||||
open,
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
status,
|
||||
seek,
|
||||
volume
|
||||
}
|
||||
|
||||
function getDevices () {
|
||||
return [{name: type + '-1'}, {name: type + '-2'}]
|
||||
}
|
||||
|
||||
function open () {}
|
||||
function play () {}
|
||||
function pause () {}
|
||||
function stop () {}
|
||||
function status () {}
|
||||
function seek () {}
|
||||
function volume () {}
|
||||
}
|
||||
|
||||
// chromecast player implementation
|
||||
function chromecastPlayer () {
|
||||
var ret = {
|
||||
const ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
@@ -95,8 +131,8 @@ function chromecastPlayer () {
|
||||
}
|
||||
|
||||
function open () {
|
||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||
ret.device.play(state.server.networkURL, {
|
||||
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
|
||||
type: 'video/mp4',
|
||||
title: config.APP_NAME + ' - ' + torrentSummary.name
|
||||
}, function (err) {
|
||||
@@ -126,13 +162,7 @@ function chromecastPlayer () {
|
||||
}
|
||||
|
||||
function status () {
|
||||
ret.device.status(function (err, status) {
|
||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||
state.playing.currentTime = status.currentTime
|
||||
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
||||
update()
|
||||
})
|
||||
ret.device.status(handleStatus)
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
@@ -146,7 +176,7 @@ function chromecastPlayer () {
|
||||
|
||||
// airplay player implementation
|
||||
function airplayPlayer () {
|
||||
var ret = {
|
||||
const ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
@@ -183,7 +213,7 @@ function airplayPlayer () {
|
||||
}
|
||||
|
||||
function open () {
|
||||
ret.device.play(state.server.networkURL, function (err, res) {
|
||||
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
@@ -238,7 +268,7 @@ function airplayPlayer () {
|
||||
|
||||
// DLNA player implementation
|
||||
function dlnaPlayer (player) {
|
||||
var ret = {
|
||||
const ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
@@ -274,8 +304,8 @@ function dlnaPlayer (player) {
|
||||
}
|
||||
|
||||
function open () {
|
||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||
ret.device.play(state.server.networkURL, {
|
||||
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
|
||||
type: 'video/mp4',
|
||||
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
||||
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
||||
@@ -306,13 +336,7 @@ function dlnaPlayer (player) {
|
||||
}
|
||||
|
||||
function status () {
|
||||
ret.device.status(function (err, status) {
|
||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||
state.playing.currentTime = status.currentTime
|
||||
state.playing.volume = status.volume.level
|
||||
update()
|
||||
})
|
||||
ret.device.status(handleStatus)
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
@@ -328,10 +352,22 @@ function dlnaPlayer (player) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatus (err, status) {
|
||||
if (err || !status) {
|
||||
return console.log('error getting %s status: %o',
|
||||
state.playing.location,
|
||||
err || 'missing response')
|
||||
}
|
||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||
state.playing.currentTime = status.currentTime
|
||||
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
||||
update()
|
||||
}
|
||||
|
||||
// Start polling cast device state, whenever we're connected
|
||||
function startStatusInterval () {
|
||||
statusInterval = setInterval(function () {
|
||||
var player = getPlayer()
|
||||
const player = getPlayer()
|
||||
if (player) player.status()
|
||||
}, 1000)
|
||||
}
|
||||
@@ -350,23 +386,26 @@ function toggleMenu (location) {
|
||||
|
||||
// Never cast to two devices at the same time
|
||||
if (state.playing.location !== 'local') {
|
||||
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
|
||||
}
|
||||
throw new CastingError(
|
||||
`You can't connect to ${location} when already connected to another device`
|
||||
) }
|
||||
|
||||
// Find all cast devices of the given type
|
||||
var player = getPlayer(location)
|
||||
var devices = player ? player.getDevices() : []
|
||||
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
||||
const player = getPlayer(location)
|
||||
const devices = player ? player.getDevices() : []
|
||||
if (devices.length === 0) {
|
||||
throw new CastingError(`No ${location} devices available`)
|
||||
}
|
||||
|
||||
// Show a menu
|
||||
state.devices.castMenu = {location, devices}
|
||||
}
|
||||
|
||||
function selectDevice (index) {
|
||||
var {location, devices} = state.devices.castMenu
|
||||
const {location, devices} = state.devices.castMenu
|
||||
|
||||
// Start casting
|
||||
var player = getPlayer(location)
|
||||
const player = getPlayer(location)
|
||||
player.device = devices[index]
|
||||
player.open()
|
||||
|
||||
@@ -382,7 +421,7 @@ function selectDevice (index) {
|
||||
|
||||
// Stops casting, move video back to local screen
|
||||
function stop () {
|
||||
var player = getPlayer()
|
||||
const player = getPlayer()
|
||||
if (player) {
|
||||
player.stop(function () {
|
||||
player.device = null
|
||||
@@ -417,18 +456,18 @@ function getPlayer (location) {
|
||||
}
|
||||
|
||||
function play () {
|
||||
var player = getPlayer()
|
||||
const player = getPlayer()
|
||||
if (player) player.play(castCallback)
|
||||
}
|
||||
|
||||
function pause () {
|
||||
var player = getPlayer()
|
||||
const player = getPlayer()
|
||||
if (player) player.pause(castCallback)
|
||||
}
|
||||
|
||||
function setRate (rate) {
|
||||
var player
|
||||
var result = true
|
||||
let player
|
||||
let result = true
|
||||
if (state.playing.location === 'chromecast') {
|
||||
// TODO find how to control playback rate on chromecast
|
||||
castCallback()
|
||||
@@ -443,12 +482,12 @@ function setRate (rate) {
|
||||
}
|
||||
|
||||
function seek (time) {
|
||||
var player = getPlayer()
|
||||
const player = getPlayer()
|
||||
if (player) player.seek(time, castCallback)
|
||||
}
|
||||
|
||||
function setVolume (volume) {
|
||||
var player = getPlayer()
|
||||
const player = getPlayer()
|
||||
if (player) player.volume(volume, castCallback)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ module.exports = {
|
||||
setDispatch
|
||||
}
|
||||
|
||||
var dispatchers = {}
|
||||
var _dispatch = function () {}
|
||||
const dispatchers = {}
|
||||
let _dispatch = function () {}
|
||||
|
||||
function setDispatch (dispatch) {
|
||||
_dispatch = dispatch
|
||||
@@ -20,8 +20,8 @@ function dispatch (...args) {
|
||||
// function. This prevents React from updating the listener functions on
|
||||
// each update().
|
||||
function dispatcher (...args) {
|
||||
var str = JSON.stringify(args)
|
||||
var handler = dispatchers[str]
|
||||
const str = JSON.stringify(args)
|
||||
let handler = dispatchers[str]
|
||||
if (!handler) {
|
||||
handler = dispatchers[str] = function (e) {
|
||||
// Do not propagate click to elements below the button
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
module.exports = {
|
||||
UnplayableError
|
||||
const ExtendableError = require('es6-error')
|
||||
|
||||
/* Generic errors */
|
||||
|
||||
class CastingError extends ExtendableError {}
|
||||
class PlaybackError extends ExtendableError {}
|
||||
class SoundError extends ExtendableError {}
|
||||
class TorrentError extends ExtendableError {}
|
||||
|
||||
/* Playback */
|
||||
|
||||
class UnplayableTorrentError extends PlaybackError {
|
||||
constructor () { super('Can\'t play any files in torrent') }
|
||||
}
|
||||
|
||||
function UnplayableError () {
|
||||
this.message = 'Can\'t play any files in torrent'
|
||||
class UnplayableFileError extends PlaybackError {
|
||||
constructor () { super('Can\'t play that file') }
|
||||
}
|
||||
|
||||
/* Sound */
|
||||
|
||||
class InvalidSoundNameError extends SoundError {
|
||||
constructor (name) { super(`Invalid sound name: ${name}`) }
|
||||
}
|
||||
|
||||
/* Torrent */
|
||||
|
||||
class TorrentKeyNotFoundError extends TorrentError {
|
||||
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CastingError,
|
||||
PlaybackError,
|
||||
SoundError,
|
||||
TorrentError,
|
||||
UnplayableTorrentError,
|
||||
UnplayableFileError,
|
||||
InvalidSoundNameError,
|
||||
TorrentKeyNotFoundError
|
||||
}
|
||||
UnplayableError.prototype = Error
|
||||
|
||||
@@ -4,10 +4,10 @@ module.exports = {
|
||||
run
|
||||
}
|
||||
|
||||
const semver = require('semver')
|
||||
const config = require('../../config')
|
||||
const TorrentSummary = require('./torrent-summary')
|
||||
const fs = require('fs')
|
||||
const semver = require('semver')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
// Change `state.saved` (which will be saved back to config.json on exit) as
|
||||
// needed, for example to deal with config.json format changes across versions
|
||||
@@ -17,7 +17,7 @@ function run (state) {
|
||||
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
|
||||
}
|
||||
|
||||
var version = state.saved.version
|
||||
const version = state.saved.version
|
||||
|
||||
if (semver.lt(version, '0.7.0')) {
|
||||
migrate_0_7_0(state.saved)
|
||||
@@ -35,16 +35,24 @@ function run (state) {
|
||||
migrate_0_12_0(state.saved)
|
||||
}
|
||||
|
||||
if (semver.lt(version, '0.14.0')) {
|
||||
migrate_0_14_0(state.saved)
|
||||
}
|
||||
|
||||
if (semver.lt(version, '0.17.0')) {
|
||||
migrate_0_17_0(state.saved)
|
||||
}
|
||||
|
||||
// Config is now on the new version
|
||||
state.saved.version = config.APP_VERSION
|
||||
}
|
||||
|
||||
function migrate_0_7_0 (saved) {
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
const cpFile = require('cp-file')
|
||||
const path = require('path')
|
||||
|
||||
saved.torrents.forEach(function (ts) {
|
||||
var infoHash = ts.infoHash
|
||||
const infoHash = ts.infoHash
|
||||
|
||||
// Replace torrentPath with torrentFileName
|
||||
// There are a number of cases to handle here:
|
||||
@@ -52,7 +60,7 @@ function migrate_0_7_0 (saved) {
|
||||
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
||||
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
||||
// * Finally, now we're getting rid of torrentPath altogether
|
||||
var src, dst
|
||||
let src, dst
|
||||
if (ts.torrentPath) {
|
||||
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||
src = ts.torrentPath
|
||||
@@ -62,7 +70,7 @@ function migrate_0_7_0 (saved) {
|
||||
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
if (src !== dst) cpFile.sync(src, dst)
|
||||
|
||||
delete ts.torrentPath
|
||||
ts.torrentFileName = infoHash + '.torrent'
|
||||
@@ -70,14 +78,14 @@ function migrate_0_7_0 (saved) {
|
||||
|
||||
// Replace posterURL with posterFileName
|
||||
if (ts.posterURL) {
|
||||
var extension = path.extname(ts.posterURL)
|
||||
const extension = path.extname(ts.posterURL)
|
||||
src = path.isAbsolute(ts.posterURL)
|
||||
? ts.posterURL
|
||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||
dst = path.join(config.POSTER_PATH, infoHash + extension)
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
if (src !== dst) cpFile.sync(src, dst)
|
||||
|
||||
delete ts.posterURL
|
||||
ts.posterFileName = infoHash + extension
|
||||
@@ -108,6 +116,8 @@ function migrate_0_11_0 (saved) {
|
||||
}
|
||||
|
||||
function migrate_0_12_0 (saved) {
|
||||
const TorrentSummary = require('./torrent-summary')
|
||||
|
||||
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
|
||||
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
|
||||
}
|
||||
@@ -116,7 +126,7 @@ function migrate_0_12_0 (saved) {
|
||||
// Undo a terrible bug where clicking Play on a default torrent on a fresh
|
||||
// install results in a "path missing" error
|
||||
// See https://github.com/feross/webtorrent-desktop/pull/806
|
||||
var defaultTorrentFiles = [
|
||||
const defaultTorrentFiles = [
|
||||
'6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent',
|
||||
'88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent',
|
||||
'6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent',
|
||||
@@ -125,13 +135,31 @@ function migrate_0_12_0 (saved) {
|
||||
]
|
||||
saved.torrents.forEach(function (torrentSummary) {
|
||||
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
|
||||
var fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
if (!fileOrFolder) return
|
||||
try {
|
||||
fs.statSync(fileOrFolder)
|
||||
} catch (e) {
|
||||
} catch (err) {
|
||||
// Default torrent with "missing path" error. Clear path.
|
||||
delete torrentSummary.path
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function migrate_0_14_0 (saved) {
|
||||
saved.torrents.forEach(function (ts) {
|
||||
delete ts.defaultPlayFileIndex
|
||||
})
|
||||
}
|
||||
|
||||
function migrate_0_17_0 (saved) {
|
||||
// Fix a sad, sad bug that resulted in 100MB+ config.json files
|
||||
saved.torrents.forEach(function (ts) {
|
||||
if (!ts.files) return
|
||||
ts.files.forEach(function (file) {
|
||||
if (!file.audioInfo || !file.audioInfo.picture) return
|
||||
// This contained a Buffer, which 30x'd in size when serialized to JSON
|
||||
delete file.audioInfo.picture
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
87
src/renderer/lib/playlist.js
Normal file
@@ -0,0 +1,87 @@
|
||||
module.exports = {
|
||||
hasNext,
|
||||
getNextIndex,
|
||||
hasPrevious,
|
||||
getPreviousIndex,
|
||||
getCurrentLocalURL
|
||||
}
|
||||
|
||||
const TorrentSummary = require('./torrent-summary')
|
||||
const TorrentPlayer = require('./torrent-player')
|
||||
|
||||
const cache = {
|
||||
infoHash: null,
|
||||
previousIndex: null,
|
||||
currentIndex: null,
|
||||
nextIndex: null
|
||||
}
|
||||
|
||||
function hasNext (state) {
|
||||
updateCache(state)
|
||||
return cache.nextIndex !== null
|
||||
}
|
||||
|
||||
function getNextIndex (state) {
|
||||
updateCache(state)
|
||||
return cache.nextIndex
|
||||
}
|
||||
|
||||
function hasPrevious (state) {
|
||||
updateCache(state)
|
||||
return cache.previousIndex !== null
|
||||
}
|
||||
|
||||
function getPreviousIndex (state) {
|
||||
updateCache(state)
|
||||
return cache.previousIndex
|
||||
}
|
||||
|
||||
function getCurrentLocalURL (state) {
|
||||
return state.server
|
||||
? state.server.localURL + '/' + state.playing.fileIndex
|
||||
: ''
|
||||
}
|
||||
|
||||
function updateCache (state) {
|
||||
const infoHash = state.playing.infoHash
|
||||
const fileIndex = state.playing.fileIndex
|
||||
|
||||
if (infoHash === cache.infoHash) {
|
||||
switch (fileIndex) {
|
||||
case cache.currentIndex:
|
||||
return
|
||||
case cache.nextIndex:
|
||||
cache.previousIndex = cache.currentIndex
|
||||
cache.currentIndex = fileIndex
|
||||
cache.nextIndex = findNextIndex(state)
|
||||
return
|
||||
case cache.previousIndex:
|
||||
cache.previousIndex = findPreviousIndex(state)
|
||||
cache.nextIndex = cache.currentIndex
|
||||
cache.currentIndex = fileIndex
|
||||
return
|
||||
}
|
||||
} else {
|
||||
cache.infoHash = infoHash
|
||||
}
|
||||
|
||||
cache.previousIndex = findPreviousIndex(state)
|
||||
cache.currentIndex = fileIndex
|
||||
cache.nextIndex = findNextIndex(state)
|
||||
}
|
||||
|
||||
function findPreviousIndex (state) {
|
||||
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
||||
for (let i = state.playing.fileIndex - 1; i >= 0; i--) {
|
||||
if (TorrentPlayer.isPlayable(files[i])) return i
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function findNextIndex (state) {
|
||||
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
||||
for (let i = state.playing.fileIndex + 1; i < files.length; i++) {
|
||||
if (TorrentPlayer.isPlayable(files[i])) return i
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
module.exports = {
|
||||
preload,
|
||||
play
|
||||
}
|
||||
|
||||
var config = require('../../config')
|
||||
var path = require('path')
|
||||
const config = require('../../config')
|
||||
const {InvalidSoundNameError} = require('./errors')
|
||||
const path = require('path')
|
||||
|
||||
var VOLUME = 0.15
|
||||
const VOLUME = 0.25
|
||||
|
||||
/* Cache of Audio elements, for instant playback */
|
||||
var cache = {}
|
||||
const cache = {}
|
||||
|
||||
var sounds = {
|
||||
const sounds = {
|
||||
ADD: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
|
||||
volume: VOLUME
|
||||
},
|
||||
DELETE: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
||||
volume: VOLUME
|
||||
volume: VOLUME * 0.5
|
||||
},
|
||||
DISABLE: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
||||
volume: VOLUME
|
||||
volume: VOLUME * 0.5
|
||||
},
|
||||
DONE: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
|
||||
@@ -30,7 +30,7 @@ var sounds = {
|
||||
},
|
||||
ENABLE: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
|
||||
volume: VOLUME
|
||||
volume: VOLUME * 0.5
|
||||
},
|
||||
ERROR: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
|
||||
@@ -42,27 +42,16 @@ var sounds = {
|
||||
},
|
||||
STARTUP: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
||||
volume: VOLUME * 2
|
||||
}
|
||||
}
|
||||
|
||||
function preload () {
|
||||
for (var name in sounds) {
|
||||
if (!cache[name]) {
|
||||
var sound = sounds[name]
|
||||
var audio = cache[name] = new window.Audio()
|
||||
audio.volume = sound.volume
|
||||
audio.src = sound.url
|
||||
}
|
||||
volume: VOLUME
|
||||
}
|
||||
}
|
||||
|
||||
function play (name) {
|
||||
var audio = cache[name]
|
||||
let audio = cache[name]
|
||||
if (!audio) {
|
||||
var sound = sounds[name]
|
||||
const sound = sounds[name]
|
||||
if (!sound) {
|
||||
throw new Error('Invalid sound name')
|
||||
throw new InvalidSoundNameError(name)
|
||||
}
|
||||
audio = cache[name] = new window.Audio()
|
||||
audio.volume = sound.volume
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
var appConfig = require('application-config')('WebTorrent')
|
||||
var path = require('path')
|
||||
var {EventEmitter} = require('events')
|
||||
const appConfig = require('application-config')('WebTorrent')
|
||||
const path = require('path')
|
||||
const {EventEmitter} = require('events')
|
||||
|
||||
var config = require('../../config')
|
||||
var migrations = require('./migrations')
|
||||
const config = require('../../config')
|
||||
|
||||
var State = module.exports = Object.assign(new EventEmitter(), {
|
||||
getDefaultPlayState,
|
||||
load,
|
||||
save,
|
||||
saveThrottled
|
||||
})
|
||||
const SAVE_DEBOUNCE_INTERVAL = 1000
|
||||
|
||||
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||
|
||||
const State = module.exports = Object.assign(new EventEmitter(), {
|
||||
getDefaultPlayState,
|
||||
load,
|
||||
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
|
||||
save: function () {
|
||||
// Perf optimization: Lazy-require debounce (and it's dependencies)
|
||||
const debounce = require('debounce')
|
||||
// After first State.save() invokation, future calls go straight to the
|
||||
// debounced function
|
||||
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
|
||||
State.save(...arguments)
|
||||
},
|
||||
saveImmediate
|
||||
})
|
||||
|
||||
function getDefaultState () {
|
||||
var LocationHistory = require('location-history')
|
||||
const LocationHistory = require('location-history')
|
||||
|
||||
return {
|
||||
/*
|
||||
@@ -24,7 +33,11 @@ function getDefaultState () {
|
||||
*/
|
||||
client: null, /* the WebTorrent client */
|
||||
server: null, /* local WebTorrent-to-HTTP server */
|
||||
prev: {}, /* used for state diffing in updateElectron() */
|
||||
prev: { /* used for state diffing in updateElectron() */
|
||||
title: null,
|
||||
progress: -1,
|
||||
badge: null
|
||||
},
|
||||
location: new LocationHistory(),
|
||||
window: {
|
||||
bounds: null, /* {x, y, width, height } */
|
||||
@@ -64,7 +77,9 @@ function getDefaultState () {
|
||||
* Getters, for convenience
|
||||
*/
|
||||
getPlayingTorrentSummary,
|
||||
getPlayingFileSummary
|
||||
getPlayingFileSummary,
|
||||
getExternalPlayerName,
|
||||
shouldHidePlayerControls
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,39 +108,39 @@ function getDefaultPlayState () {
|
||||
}
|
||||
|
||||
/* If the saved state file doesn't exist yet, here's what we use instead */
|
||||
function setupSavedState (cb) {
|
||||
var fs = require('fs-extra')
|
||||
var parseTorrent = require('parse-torrent')
|
||||
var parallel = require('run-parallel')
|
||||
function setupStateSaved (cb) {
|
||||
const cpFile = require('cp-file')
|
||||
const fs = require('fs')
|
||||
const parseTorrent = require('parse-torrent')
|
||||
const parallel = require('run-parallel')
|
||||
|
||||
var saved = {
|
||||
const saved = {
|
||||
prefs: {
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
||||
isFileHandler: false,
|
||||
openExternalPlayer: false,
|
||||
externalPlayerPath: null
|
||||
externalPlayerPath: null,
|
||||
startup: false
|
||||
},
|
||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||
}
|
||||
|
||||
var tasks = []
|
||||
const tasks = []
|
||||
|
||||
config.DEFAULT_TORRENTS.map(function (t, i) {
|
||||
var infoHash = saved.torrents[i].infoHash
|
||||
const infoHash = saved.torrents[i].infoHash
|
||||
tasks.push(function (cb) {
|
||||
fs.copy(
|
||||
cpFile(
|
||||
path.join(config.STATIC_PATH, t.posterFileName),
|
||||
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)),
|
||||
cb
|
||||
)
|
||||
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
|
||||
).then(cb).catch(cb)
|
||||
})
|
||||
tasks.push(function (cb) {
|
||||
fs.copy(
|
||||
cpFile(
|
||||
path.join(config.STATIC_PATH, t.torrentFileName),
|
||||
path.join(config.TORRENT_PATH, infoHash + '.torrent'),
|
||||
cb
|
||||
)
|
||||
path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||
).then(cb).catch(cb)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -135,8 +150,9 @@ function setupSavedState (cb) {
|
||||
})
|
||||
|
||||
function createTorrentObject (t) {
|
||||
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
||||
var parsedTorrent = parseTorrent(torrent)
|
||||
// TODO: Doing several fs.readFileSync calls during first startup is not ideal
|
||||
const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
||||
const parsedTorrent = parseTorrent(torrent)
|
||||
|
||||
return {
|
||||
status: 'paused',
|
||||
@@ -147,62 +163,80 @@ function setupSavedState (cb) {
|
||||
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
||||
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
||||
files: parsedTorrent.files,
|
||||
selections: parsedTorrent.files.map((x) => true)
|
||||
selections: parsedTorrent.files.map((x) => true),
|
||||
testID: t.testID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPlayingTorrentSummary () {
|
||||
var infoHash = this.playing.infoHash
|
||||
const infoHash = this.playing.infoHash
|
||||
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||
}
|
||||
|
||||
function getPlayingFileSummary () {
|
||||
var torrentSummary = this.getPlayingTorrentSummary()
|
||||
const torrentSummary = this.getPlayingTorrentSummary()
|
||||
if (!torrentSummary) return null
|
||||
return torrentSummary.files[this.playing.fileIndex]
|
||||
}
|
||||
|
||||
function load (cb) {
|
||||
var state = getDefaultState()
|
||||
function getExternalPlayerName () {
|
||||
const playerPath = this.saved.prefs.externalPlayerPath
|
||||
if (!playerPath) return 'VLC'
|
||||
return path.basename(playerPath).split('.')[0]
|
||||
}
|
||||
|
||||
function shouldHidePlayerControls () {
|
||||
return this.location.url() === 'player' &&
|
||||
this.playing.mouseStationarySince !== 0 &&
|
||||
new Date().getTime() - this.playing.mouseStationarySince > 2000 &&
|
||||
!this.playing.mouseInControls &&
|
||||
!this.playing.isPaused &&
|
||||
this.playing.location === 'local' &&
|
||||
this.playing.playbackRate === 1
|
||||
}
|
||||
|
||||
function load (cb) {
|
||||
appConfig.read(function (err, saved) {
|
||||
if (err || !saved.version) {
|
||||
console.log('Missing config file: Creating new one')
|
||||
setupSavedState(onSaved)
|
||||
setupStateSaved(onSavedState)
|
||||
} else {
|
||||
onSaved(null, saved)
|
||||
onSavedState(null, saved)
|
||||
}
|
||||
})
|
||||
|
||||
function onSaved (err, saved) {
|
||||
function onSavedState (err, saved) {
|
||||
if (err) return cb(err)
|
||||
const state = getDefaultState()
|
||||
state.saved = saved
|
||||
migrations.run(state)
|
||||
|
||||
if (process.type === 'renderer') {
|
||||
// Perf optimization: Save require() calls in the main process
|
||||
const migrations = require('./migrations')
|
||||
migrations.run(state)
|
||||
}
|
||||
|
||||
cb(null, state)
|
||||
}
|
||||
}
|
||||
|
||||
// Write state.saved to the JSON state file
|
||||
function save (state, cb) {
|
||||
function saveImmediate (state, cb) {
|
||||
console.log('Saving state to ' + appConfig.filePath)
|
||||
delete state.saveStateTimeout
|
||||
|
||||
// Clean up, so that we're not saving any pending state
|
||||
var copy = Object.assign({}, state.saved)
|
||||
const copy = Object.assign({}, state.saved)
|
||||
// Remove torrents pending addition to the list, where we haven't finished
|
||||
// reading the torrent file or file(s) to seed & don't have an infohash
|
||||
copy.torrents = copy.torrents
|
||||
.filter((x) => x.infoHash)
|
||||
.map(function (x) {
|
||||
var torrent = {}
|
||||
for (var key in x) {
|
||||
const torrent = {}
|
||||
for (let key in x) {
|
||||
if (key === 'progress' || key === 'torrentKey') {
|
||||
continue // Don't save progress info or key for the webtorrent process
|
||||
}
|
||||
if (key === 'playStatus') {
|
||||
continue // Don't save whether a torrent is playing / pending
|
||||
}
|
||||
if (key === 'error') {
|
||||
continue // Don't save error states
|
||||
}
|
||||
@@ -213,15 +247,6 @@ function save (state, cb) {
|
||||
|
||||
appConfig.write(copy, (err) => {
|
||||
if (err) console.error(err)
|
||||
else State.emit('savedState')
|
||||
else State.emit('stateSaved')
|
||||
})
|
||||
}
|
||||
|
||||
// Write, but no more than once a second
|
||||
function saveThrottled (state) {
|
||||
if (state.saveStateTimeout) return
|
||||
state.saveStateTimeout = setTimeout(function () {
|
||||
if (!state.saveStateTimeout) return
|
||||
save(state)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
@@ -2,49 +2,68 @@
|
||||
// Reports back so that we can improve WebTorrent Desktop
|
||||
module.exports = {
|
||||
init,
|
||||
send,
|
||||
logUncaughtError,
|
||||
logPlayAttempt
|
||||
}
|
||||
|
||||
const crypto = require('crypto')
|
||||
const electron = require('electron')
|
||||
const https = require('https')
|
||||
const os = require('os')
|
||||
const url = require('url')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
var telemetry
|
||||
let telemetry
|
||||
|
||||
function init (state) {
|
||||
telemetry = state.saved.telemetry
|
||||
|
||||
// First app run
|
||||
if (!telemetry) {
|
||||
telemetry = state.saved.telemetry = createSummary()
|
||||
const crypto = require('crypto')
|
||||
telemetry = state.saved.telemetry = {
|
||||
userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID
|
||||
}
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
var now = new Date()
|
||||
function send (state) {
|
||||
const now = new Date()
|
||||
telemetry.version = config.APP_VERSION
|
||||
telemetry.timestamp = now.toISOString()
|
||||
telemetry.localTime = now.toTimeString()
|
||||
telemetry.screens = getScreenInfo()
|
||||
telemetry.system = getSystemInfo()
|
||||
telemetry.approxNumTorrents = getApproxNumTorrents(state)
|
||||
telemetry.torrentStats = getTorrentStats(state)
|
||||
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
|
||||
|
||||
if (config.IS_PRODUCTION) {
|
||||
postToServer()
|
||||
// If the user keeps WebTorrent running for a long time, post every 24h
|
||||
setInterval(postToServer, 12 * 3600 * 1000)
|
||||
} else {
|
||||
if (!config.IS_PRODUCTION) {
|
||||
// Development: telemetry used only for local debugging
|
||||
// Empty uncaught errors, etc at the start of every run
|
||||
reset()
|
||||
return reset()
|
||||
}
|
||||
|
||||
const get = require('simple-get')
|
||||
|
||||
const opts = {
|
||||
url: config.TELEMETRY_URL,
|
||||
body: telemetry,
|
||||
json: true
|
||||
}
|
||||
|
||||
get.post(opts, function (err, res) {
|
||||
if (err) return console.error('Error sending telemetry', err)
|
||||
if (res.statusCode !== 200) {
|
||||
return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
|
||||
}
|
||||
console.log('Sent telemetry')
|
||||
reset()
|
||||
})
|
||||
}
|
||||
|
||||
function reset () {
|
||||
telemetry.uncaughtErrors = []
|
||||
telemetry.playAttempts = {
|
||||
minVersion: config.APP_VERSION,
|
||||
total: 0,
|
||||
success: 0,
|
||||
timeout: 0,
|
||||
@@ -53,41 +72,6 @@ function reset () {
|
||||
}
|
||||
}
|
||||
|
||||
function postToServer () {
|
||||
// Serialize the telemetry summary
|
||||
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
|
||||
|
||||
// POST to our server
|
||||
var options = url.parse(config.TELEMETRY_URL)
|
||||
options.method = 'POST'
|
||||
options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': payload.length
|
||||
}
|
||||
|
||||
var req = https.request(options, function (res) {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('Successfully posted telemetry summary')
|
||||
reset()
|
||||
} else {
|
||||
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
|
||||
}
|
||||
})
|
||||
req.on('error', function (e) {
|
||||
console.error('Couldn\'t post telemetry summary', e)
|
||||
})
|
||||
req.write(payload)
|
||||
req.end()
|
||||
}
|
||||
|
||||
// Creates a new telemetry summary. Gives the user a unique ID,
|
||||
// collects screen resolution, etc
|
||||
function createSummary () {
|
||||
// Make a 256-bit random unique ID
|
||||
var userID = crypto.randomBytes(32).toString('hex')
|
||||
return { userID }
|
||||
}
|
||||
|
||||
// Track screen resolution
|
||||
function getScreenInfo () {
|
||||
return electron.screen.getAllDisplays().map((screen) => ({
|
||||
@@ -99,48 +83,131 @@ function getScreenInfo () {
|
||||
|
||||
// Track basic system info like OS version and amount of RAM
|
||||
function getSystemInfo () {
|
||||
const os = require('os')
|
||||
return {
|
||||
osPlatform: process.platform,
|
||||
osRelease: os.type() + ' ' + os.release(),
|
||||
architecture: os.arch(),
|
||||
totalMemoryMB: os.totalmem() / (1 << 20),
|
||||
systemArchitecture: config.OS_SYSARCH,
|
||||
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
|
||||
numCores: os.cpus().length
|
||||
}
|
||||
}
|
||||
|
||||
// Get the number of torrents, rounded to the nearest power of two
|
||||
function getApproxNumTorrents (state) {
|
||||
var exactNum = state.saved.torrents.length
|
||||
if (exactNum === 0) return 0
|
||||
// Get stats like the # of torrents currently active, # in list, total size
|
||||
function getTorrentStats (state) {
|
||||
const count = state.saved.torrents.length
|
||||
let sizeMB = 0
|
||||
let byStatus = {
|
||||
new: { count: 0, sizeMB: 0 },
|
||||
downloading: { count: 0, sizeMB: 0 },
|
||||
seeding: { count: 0, sizeMB: 0 },
|
||||
paused: { count: 0, sizeMB: 0 }
|
||||
}
|
||||
|
||||
// First, count torrents & total file size
|
||||
for (let i = 0; i < count; i++) {
|
||||
const t = state.saved.torrents[i]
|
||||
const stat = byStatus[t.status]
|
||||
if (!t || !t.files || !stat) continue
|
||||
stat.count++
|
||||
for (let j = 0; j < t.files.length; j++) {
|
||||
const f = t.files[j]
|
||||
if (!f || !f.length) continue
|
||||
const fileSizeMB = f.length / (1 << 20)
|
||||
sizeMB += fileSizeMB
|
||||
stat.sizeMB += fileSizeMB
|
||||
}
|
||||
}
|
||||
|
||||
// Then, round all the counts and sums to the nearest power of 2
|
||||
const ret = roundTorrentStats({count, sizeMB})
|
||||
ret.byStatus = {
|
||||
new: roundTorrentStats(byStatus.new),
|
||||
downloading: roundTorrentStats(byStatus.downloading),
|
||||
seeding: roundTorrentStats(byStatus.seeding),
|
||||
paused: roundTorrentStats(byStatus.paused)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
function roundTorrentStats (stats) {
|
||||
return {
|
||||
approxCount: roundPow2(stats.count),
|
||||
approxSizeMB: roundPow2(stats.sizeMB)
|
||||
}
|
||||
}
|
||||
|
||||
// Rounds to the nearest power of 2, for privacy and easy bucketing.
|
||||
// Rounds 35 to 32, 70 to 64, 5 to 4, 1 to 1, 0 to 0.
|
||||
// Supports nonnegative numbers only.
|
||||
function roundPow2 (n) {
|
||||
if (n <= 0) return 0
|
||||
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||
var log2 = Math.log(exactNum) / Math.log(2)
|
||||
return 1 << Math.round(log2)
|
||||
const log2 = Math.log(n) / Math.log(2)
|
||||
return Math.pow(2, Math.round(log2))
|
||||
}
|
||||
|
||||
// An uncaught error happened in the main process or in one of the windows
|
||||
function logUncaughtError (procName, err) {
|
||||
function logUncaughtError (procName, e) {
|
||||
// Not initialized yet? Ignore.
|
||||
// Hopefully uncaught errors immediately on startup are fixed in dev
|
||||
if (!telemetry) return
|
||||
|
||||
var message, stack
|
||||
if (err instanceof Error) {
|
||||
message = err.message
|
||||
// Remove the first part of each file path in the stack trace.
|
||||
// - Privacy: remove personal info like C:\Users\<full name>
|
||||
// - Aggregation: this lets us find which stacktraces occur often
|
||||
stack = err.stack.replace(/\(.*app.asar/g, '(...')
|
||||
let message
|
||||
let stack = ''
|
||||
if (e == null) {
|
||||
message = 'Unexpected undefined error'
|
||||
} else if (e.error) {
|
||||
// Uncaught Javascript errors (window.onerror), err is an ErrorEvent
|
||||
if (!e.error.message) {
|
||||
message = 'Unexpected ErrorEvent.error: ' + Object.keys(e.error).join(' ')
|
||||
} else {
|
||||
message = e.error.message
|
||||
stack = e.error.stack
|
||||
}
|
||||
} else if (e.message) {
|
||||
// err is either an Error or a plain object {message, stack}
|
||||
message = e.message
|
||||
stack = e.stack
|
||||
} else {
|
||||
message = String(err)
|
||||
stack = ''
|
||||
// Resource errors (captured element.onerror), err is an Event
|
||||
if (!e.target) {
|
||||
message = 'Unexpected unknown error'
|
||||
} else if (!e.target.error) {
|
||||
message = 'Unexpected resource loading error: ' + getElemString(e.target)
|
||||
} else {
|
||||
message = 'Resource error ' + getElemString(e.target) + ': ' + e.target.error.code
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof stack !== 'string') stack = 'Unexpected stack: ' + stack
|
||||
if (typeof message !== 'string') message = 'Unexpected message: ' + message
|
||||
|
||||
// Remove the first part of each file path in the stack trace.
|
||||
// - Privacy: remove personal info like C:\Users\<full name>
|
||||
// - Aggregation: this lets us find which stacktraces occur often
|
||||
stack = stack.replace(/\(.*app.asar/g, '(...')
|
||||
stack = stack.replace(/at .*app.asar/g, 'at ...')
|
||||
|
||||
// We need to POST the telemetry object, make sure it stays < 100kb
|
||||
if (telemetry.uncaughtErrors.length > 20) return
|
||||
if (message.length > 1000) message = message.substring(0, 1000)
|
||||
if (stack.length > 1000) stack = stack.substring(0, 1000)
|
||||
|
||||
telemetry.uncaughtErrors.push({process: procName, message, stack})
|
||||
// Log the app version *at the time of the error*
|
||||
const version = config.APP_VERSION
|
||||
|
||||
telemetry.uncaughtErrors.push({process: procName, message, stack, version})
|
||||
}
|
||||
|
||||
// Turns a DOM element into a string, eg "DIV.my-class.visible"
|
||||
function getElemString (elem) {
|
||||
let ret = elem.tagName
|
||||
try {
|
||||
ret += '.' + Array.from(elem.classList).join('.')
|
||||
} catch (err) {}
|
||||
return ret
|
||||
}
|
||||
|
||||
// The user pressed play. It either worked, timed out, or showed the
|
||||
@@ -150,7 +217,7 @@ function logPlayAttempt (result) {
|
||||
return console.error('Unknown play attempt result', result)
|
||||
}
|
||||
|
||||
var attempts = telemetry.playAttempts
|
||||
const attempts = telemetry.playAttempts
|
||||
attempts.total = (attempts.total || 0) + 1
|
||||
attempts[result] = (attempts[result] || 0) + 1
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ module.exports = {
|
||||
isVideo,
|
||||
isAudio,
|
||||
isTorrent,
|
||||
isPlayableTorrentSummary,
|
||||
pickFileToPlay
|
||||
isPlayableTorrentSummary
|
||||
}
|
||||
|
||||
var path = require('path')
|
||||
const path = require('path')
|
||||
|
||||
// Checks whether a fileSummary or file path is audio/video that we can play,
|
||||
// based on the file extension
|
||||
@@ -37,7 +36,8 @@ function isAudio (file) {
|
||||
'.ac3',
|
||||
'.mp3',
|
||||
'.ogg',
|
||||
'.wav'
|
||||
'.wav',
|
||||
'.m4a'
|
||||
].includes(getFileExtension(file))
|
||||
}
|
||||
|
||||
@@ -46,38 +46,16 @@ function isAudio (file) {
|
||||
// - a file object where obj.name is ends in .torrent
|
||||
// - a string that's a magnet link (magnet://...)
|
||||
function isTorrent (file) {
|
||||
var isTorrentFile = getFileExtension(file) === '.torrent'
|
||||
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
||||
const isTorrentFile = getFileExtension(file) === '.torrent'
|
||||
const isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
||||
return isTorrentFile || isMagnet
|
||||
}
|
||||
|
||||
function getFileExtension (file) {
|
||||
var name = typeof file === 'string' ? file : file.name
|
||||
const name = typeof file === 'string' ? file : file.name
|
||||
return path.extname(name).toLowerCase()
|
||||
}
|
||||
|
||||
function isPlayableTorrentSummary (torrentSummary) {
|
||||
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
||||
}
|
||||
|
||||
// Picks the default file to play from a list of torrent or torrentSummary files
|
||||
// Returns an index or undefined, if no files are playable
|
||||
function pickFileToPlay (files) {
|
||||
// first, try to find the biggest video file
|
||||
var videoFiles = files.filter(isVideo)
|
||||
if (videoFiles.length > 0) {
|
||||
var largestVideoFile = videoFiles.reduce(function (a, b) {
|
||||
return a.length > b.length ? a : b
|
||||
})
|
||||
return files.indexOf(largestVideoFile)
|
||||
}
|
||||
|
||||
// if there are no videos, play the first audio file
|
||||
var audioFiles = files.filter(isAudio)
|
||||
if (audioFiles.length > 0) {
|
||||
return files.indexOf(audioFiles[0])
|
||||
}
|
||||
|
||||
// no video or audio means nothing is playable
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
module.exports = torrentPoster
|
||||
|
||||
var captureVideoFrame = require('./capture-video-frame')
|
||||
var path = require('path')
|
||||
const captureFrame = require('capture-frame')
|
||||
const path = require('path')
|
||||
|
||||
function torrentPoster (torrent, cb) {
|
||||
// First, try to use a poster image if available
|
||||
var posterFile = torrent.files.filter(function (file) {
|
||||
const posterFile = torrent.files.filter(function (file) {
|
||||
return /^poster\.(jpg|png|gif)$/.test(file.name)
|
||||
})[0]
|
||||
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
|
||||
|
||||
// Second, try to use the largest video file
|
||||
// Filter out file formats that the <video> tag definitely can't play
|
||||
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
||||
const videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||
|
||||
// Third, try to use the largest image file
|
||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
|
||||
const imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
|
||||
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
||||
|
||||
// TODO: generate a waveform from the largest sound file
|
||||
@@ -25,8 +25,8 @@ function torrentPoster (torrent, cb) {
|
||||
}
|
||||
|
||||
function getLargestFileByExtension (torrent, extensions) {
|
||||
var files = torrent.files.filter(function (file) {
|
||||
var extname = path.extname(file.name).toLowerCase()
|
||||
const files = torrent.files.filter(function (file) {
|
||||
const extname = path.extname(file.name).toLowerCase()
|
||||
return extensions.indexOf(extname) !== -1
|
||||
})
|
||||
if (files.length === 0) return undefined
|
||||
@@ -36,15 +36,15 @@ function getLargestFileByExtension (torrent, extensions) {
|
||||
}
|
||||
|
||||
function torrentPosterFromVideo (file, torrent, cb) {
|
||||
var index = torrent.files.indexOf(file)
|
||||
const index = torrent.files.indexOf(file)
|
||||
|
||||
var server = torrent.createServer(0)
|
||||
const server = torrent.createServer(0)
|
||||
server.listen(0, onListening)
|
||||
|
||||
function onListening () {
|
||||
var port = server.address().port
|
||||
var url = 'http://localhost:' + port + '/' + index
|
||||
var video = document.createElement('video')
|
||||
const port = server.address().port
|
||||
const url = 'http://localhost:' + port + '/' + index
|
||||
const video = document.createElement('video')
|
||||
video.addEventListener('canplay', onCanPlay)
|
||||
|
||||
video.volume = 0
|
||||
@@ -61,7 +61,7 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
||||
function onSeeked () {
|
||||
video.removeEventListener('seeked', onSeeked)
|
||||
|
||||
var buf = captureVideoFrame(video)
|
||||
const buf = captureFrame(video)
|
||||
|
||||
// unload video element
|
||||
video.pause()
|
||||
@@ -78,6 +78,6 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
||||
}
|
||||
|
||||
function torrentPosterFromImage (file, torrent, cb) {
|
||||
var extname = path.extname(file.name)
|
||||
const extname = path.extname(file.name)
|
||||
file.getBuffer((err, buf) => cb(err, buf, extname))
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ module.exports = {
|
||||
getPosterPath,
|
||||
getTorrentPath,
|
||||
getByKey,
|
||||
getTorrentID,
|
||||
getTorrentId,
|
||||
getFileOrFolder
|
||||
}
|
||||
|
||||
var path = require('path')
|
||||
var config = require('../../config')
|
||||
const path = require('path')
|
||||
const config = require('../../config')
|
||||
|
||||
// Expects a torrentSummary
|
||||
// Returns an absolute path to the torrent file, or null if unavailable
|
||||
@@ -20,7 +20,7 @@ function getTorrentPath (torrentSummary) {
|
||||
// Returns an absolute path to the poster image, or null if unavailable
|
||||
function getPosterPath (torrentSummary) {
|
||||
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
||||
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
|
||||
const posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
|
||||
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||
return posterPath.replace(/\\/g, '/')
|
||||
@@ -28,8 +28,8 @@ function getPosterPath (torrentSummary) {
|
||||
|
||||
// Expects a torrentSummary
|
||||
// Returns a torrentID: filename, magnet URI, or infohash
|
||||
function getTorrentID (torrentSummary) {
|
||||
var s = torrentSummary
|
||||
function getTorrentId (torrentSummary) {
|
||||
const s = torrentSummary
|
||||
if (s.torrentFileName) { // Load torrent file from disk
|
||||
return getTorrentPath(s)
|
||||
} else { // Load torrent from DHT
|
||||
@@ -51,7 +51,8 @@ function getByKey (state, torrentKey) {
|
||||
// TODO: make this assumption explicit, enforce it in the `create-torrent`
|
||||
// module. Store root folder explicitly to avoid hacky path processing below.
|
||||
function getFileOrFolder (torrentSummary) {
|
||||
var ts = torrentSummary
|
||||
const ts = torrentSummary
|
||||
if (!ts.path || !ts.files || ts.files.length === 0) return null
|
||||
return path.join(ts.path, ts.files[0].path.split('/')[0])
|
||||
const dirname = ts.files[0].path.split(path.sep)[0]
|
||||
return path.join(ts.path, dirname)
|
||||
}
|
||||
|
||||
@@ -1,69 +1,121 @@
|
||||
/**
|
||||
* Perf optimization: Hook into require() to modify how certain modules load:
|
||||
*
|
||||
* - `lodash/merge` (used by `material-ui`) causes 119 require() calls at startup,
|
||||
* which take ~100ms. Replace it with `lodash.merge` which is equivalent.
|
||||
* See: https://github.com/callemall/material-ui/pull/4380#issuecomment-250894552
|
||||
*
|
||||
* - `inline-style-prefixer` (used by `material-ui`) takes ~40ms. It is not
|
||||
* actually used because auto-prefixing is disabled with
|
||||
* `darkBaseTheme.userAgent = false`. Return a fake object.
|
||||
*/
|
||||
let Module = require('module')
|
||||
const _require = Module.prototype.require
|
||||
Module.prototype.require = function (id) {
|
||||
if (id === 'lodash/merge') id = 'lodash.merge'
|
||||
if (id === 'inline-style-prefixer') return {}
|
||||
return _require.apply(this, arguments)
|
||||
}
|
||||
|
||||
console.time('init')
|
||||
|
||||
const crashReporter = require('../crash-reporter')
|
||||
crashReporter.init()
|
||||
|
||||
// Perf optimization: Start asynchronously read on config file before all the
|
||||
// blocking require() calls below.
|
||||
|
||||
const State = require('./lib/state')
|
||||
State.load(onState)
|
||||
|
||||
const createGetter = require('fn-getter')
|
||||
const dragDrop = require('drag-drop')
|
||||
const electron = require('electron')
|
||||
const fs = require('fs')
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const fs = require('fs')
|
||||
|
||||
const config = require('../config')
|
||||
const App = require('./views/app')
|
||||
const telemetry = require('./lib/telemetry')
|
||||
const sound = require('./lib/sound')
|
||||
const State = require('./lib/state')
|
||||
const TorrentPlayer = require('./lib/torrent-player')
|
||||
|
||||
const MediaController = require('./controllers/media-controller')
|
||||
const UpdateController = require('./controllers/update-controller')
|
||||
const PrefsController = require('./controllers/prefs-controller')
|
||||
const PlaybackController = require('./controllers/playback-controller')
|
||||
const SubtitlesController = require('./controllers/subtitles-controller')
|
||||
// Perf optimization: Needed immediately, so do not lazy load it below
|
||||
const TorrentListController = require('./controllers/torrent-list-controller')
|
||||
const TorrentController = require('./controllers/torrent-controller')
|
||||
|
||||
// Required by Material UI -- adds `onTouchTap` event
|
||||
require('react-tap-event-plugin')()
|
||||
|
||||
const App = require('./pages/app')
|
||||
|
||||
// Electron apps have two processes: a main process (node) runs first and starts
|
||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||
// and this IPC channel receives from and sends messages to the main process
|
||||
const ipcRenderer = electron.ipcRenderer
|
||||
|
||||
// Yo-yo pattern: state object lives here and percolates down thru all the views.
|
||||
// Events come back up from the views via dispatch(...)
|
||||
require('./lib/dispatcher').setDispatch(dispatch)
|
||||
|
||||
// From dispatch(...), events are sent to one of the controllers
|
||||
var controllers = null
|
||||
let controllers = null
|
||||
|
||||
// This dependency is the slowest-loading, so we lazy load it
|
||||
var Cast = null
|
||||
|
||||
// Electron apps have two processes: a main process (node) runs first and starts
|
||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||
// and this IPC channel receives from and sends messages to the main process
|
||||
var ipcRenderer = electron.ipcRenderer
|
||||
let Cast = null
|
||||
|
||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
||||
// All other state is ephemeral. First we load state.saved then initialize the app.
|
||||
var state
|
||||
let state
|
||||
|
||||
// Root React component
|
||||
var app
|
||||
|
||||
State.load(onState)
|
||||
let app
|
||||
|
||||
// Called once when the application loads. (Not once per window.)
|
||||
// Connects to the torrent networks, sets up the UI and OS integrations like
|
||||
// the dock icon and drag+drop.
|
||||
function onState (err, _state) {
|
||||
if (err) return onError(err)
|
||||
state = window.state = _state // Make available for easier debugging
|
||||
|
||||
// Make available for easier debugging
|
||||
state = window.state = _state
|
||||
window.dispatch = dispatch
|
||||
|
||||
telemetry.init(state)
|
||||
|
||||
// Log uncaught JS errors
|
||||
window.addEventListener(
|
||||
'error', (e) => telemetry.logUncaughtError('window', e), true /* capture */
|
||||
)
|
||||
|
||||
// Create controllers
|
||||
controllers = {
|
||||
media: new MediaController(state),
|
||||
update: new UpdateController(state),
|
||||
prefs: new PrefsController(state, config),
|
||||
playback: new PlaybackController(state, config, update),
|
||||
subtitles: new SubtitlesController(state),
|
||||
torrentList: new TorrentListController(state),
|
||||
torrent: new TorrentController(state)
|
||||
media: createGetter(() => {
|
||||
const MediaController = require('./controllers/media-controller')
|
||||
return new MediaController(state)
|
||||
}),
|
||||
playback: createGetter(() => {
|
||||
const PlaybackController = require('./controllers/playback-controller')
|
||||
return new PlaybackController(state, config, update)
|
||||
}),
|
||||
prefs: createGetter(() => {
|
||||
const PrefsController = require('./controllers/prefs-controller')
|
||||
return new PrefsController(state, config)
|
||||
}),
|
||||
subtitles: createGetter(() => {
|
||||
const SubtitlesController = require('./controllers/subtitles-controller')
|
||||
return new SubtitlesController(state)
|
||||
}),
|
||||
torrent: createGetter(() => {
|
||||
const TorrentController = require('./controllers/torrent-controller')
|
||||
return new TorrentController(state)
|
||||
}),
|
||||
torrentList: createGetter(() => {
|
||||
return new TorrentListController(state)
|
||||
}),
|
||||
update: createGetter(() => {
|
||||
const UpdateController = require('./controllers/update-controller')
|
||||
return new UpdateController(state)
|
||||
})
|
||||
}
|
||||
|
||||
// Add first page to location history
|
||||
@@ -78,23 +130,18 @@ function onState (err, _state) {
|
||||
// Restart everything we were torrenting last time the app ran
|
||||
resumeTorrents()
|
||||
|
||||
// Initialize ReactDOM
|
||||
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
|
||||
|
||||
// Calling update() updates the UI given the current state
|
||||
// Do this at least once a second to give every file in every torrentSummary
|
||||
// a progress bar and to keep the cursor in sync when playing a video
|
||||
setInterval(update, 1000)
|
||||
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
|
||||
|
||||
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
||||
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
||||
|
||||
// Listen for messages from the main process
|
||||
setupIpc()
|
||||
|
||||
// Warn if the download dir is gone, eg b/c an external drive is unplugged
|
||||
checkDownloadPath()
|
||||
|
||||
// OS integrations:
|
||||
// ...drag and drop files/text to start torrenting or seeding
|
||||
// Drag and drop files/text to start torrenting or seeding
|
||||
dragDrop('body', {
|
||||
onDrop: onOpen,
|
||||
onDropText: onOpen
|
||||
@@ -107,23 +154,33 @@ function onState (err, _state) {
|
||||
window.addEventListener('focus', onFocus)
|
||||
window.addEventListener('blur', onBlur)
|
||||
|
||||
// ...window visibility state.
|
||||
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
||||
if (electron.remote.getCurrentWindow().isVisible()) {
|
||||
sound.play('STARTUP')
|
||||
}
|
||||
|
||||
// Log uncaught JS errors
|
||||
window.addEventListener('error',
|
||||
(e) => telemetry.logUncaughtError('window', e.error || e.target), true)
|
||||
// To keep app startup fast, some code is delayed.
|
||||
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
||||
|
||||
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||
sound.play('STARTUP')
|
||||
console.timeEnd('init')
|
||||
}
|
||||
|
||||
// Runs a few seconds after the app loads, to avoid slowing down startup time
|
||||
function delayedInit () {
|
||||
telemetry.send(state)
|
||||
|
||||
// Send telemetry data every 12 hours, for users who keep the app running
|
||||
// for extended periods of time
|
||||
setInterval(() => telemetry.send(state), 12 * 3600 * 1000)
|
||||
|
||||
// Warn if the download dir is gone, eg b/c an external drive is unplugged
|
||||
checkDownloadPath()
|
||||
|
||||
// ...window visibility state.
|
||||
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
||||
onVisibilityChange()
|
||||
|
||||
lazyLoadCast()
|
||||
sound.preload()
|
||||
telemetry.init(state)
|
||||
}
|
||||
|
||||
// Lazily loads Chromecast and Airplay support
|
||||
@@ -141,7 +198,7 @@ function lazyLoadCast () {
|
||||
// 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller
|
||||
// 4. controller - the controller handles the event, changing the state object
|
||||
function update () {
|
||||
controllers.playback.showOrHidePlayerControls()
|
||||
controllers.playback().showOrHidePlayerControls()
|
||||
app.setState(state)
|
||||
updateElectron()
|
||||
}
|
||||
@@ -153,7 +210,7 @@ function updateElectron () {
|
||||
state.prev.title = state.window.title
|
||||
ipcRenderer.send('setTitle', state.window.title)
|
||||
}
|
||||
if (state.dock.progress !== state.prev.progress) {
|
||||
if (state.dock.progress.toFixed(2) !== state.prev.progress.toFixed(2)) {
|
||||
state.prev.progress = state.dock.progress
|
||||
ipcRenderer.send('setProgress', state.dock.progress)
|
||||
}
|
||||
@@ -169,43 +226,54 @@ const dispatchHandlers = {
|
||||
'openFiles': () => ipcRenderer.send('openFiles'), /* shows the open file dialog */
|
||||
'openTorrentAddress': () => { state.modal = { id: 'open-torrent-address-modal' } },
|
||||
|
||||
'addTorrent': (torrentId) => controllers.torrentList.addTorrent(torrentId),
|
||||
'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths),
|
||||
'toggleCreateTorrentAdvanced': () => controllers.torrentList.toggleCreateTorrentAdvanced(),
|
||||
'createTorrent': (options) => controllers.torrentList.createTorrent(options),
|
||||
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
|
||||
'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index),
|
||||
'confirmDeleteTorrent': (infoHash, deleteData) => controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData),
|
||||
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
||||
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
||||
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
||||
'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
|
||||
'addTorrent': (torrentId) => controllers.torrentList().addTorrent(torrentId),
|
||||
'showCreateTorrent': (paths) => controllers.torrentList().showCreateTorrent(paths),
|
||||
'createTorrent': (options) => controllers.torrentList().createTorrent(options),
|
||||
'toggleTorrent': (infoHash) => controllers.torrentList().toggleTorrent(infoHash),
|
||||
'toggleTorrentFile': (infoHash, index) =>
|
||||
controllers.torrentList().toggleTorrentFile(infoHash, index),
|
||||
'confirmDeleteTorrent': (infoHash, deleteData) =>
|
||||
controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
|
||||
'deleteTorrent': (infoHash, deleteData) =>
|
||||
controllers.torrentList().deleteTorrent(infoHash, deleteData),
|
||||
'toggleSelectTorrent': (infoHash) =>
|
||||
controllers.torrentList().toggleSelectTorrent(infoHash),
|
||||
'openTorrentContextMenu': (infoHash) =>
|
||||
controllers.torrentList().openTorrentContextMenu(infoHash),
|
||||
'startTorrentingSummary': (torrentKey) =>
|
||||
controllers.torrentList().startTorrentingSummary(torrentKey),
|
||||
'saveTorrentFileAs': (torrentKey) =>
|
||||
controllers.torrentList().saveTorrentFileAs(torrentKey),
|
||||
|
||||
// Playback
|
||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
||||
'playPause': () => controllers.playback.playPause(),
|
||||
'skip': (time) => controllers.playback.skip(time),
|
||||
'skipTo': (time) => controllers.playback.skipTo(time),
|
||||
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
|
||||
'changeVolume': (delta) => controllers.playback.changeVolume(delta),
|
||||
'setVolume': (vol) => controllers.playback.setVolume(vol),
|
||||
'openItem': (infoHash, index) => controllers.playback.openItem(infoHash, index),
|
||||
'playFile': (infoHash, index) => controllers.playback().playFile(infoHash, index),
|
||||
'playPause': () => controllers.playback().playPause(),
|
||||
'nextTrack': () => controllers.playback().nextTrack(),
|
||||
'previousTrack': () => controllers.playback().previousTrack(),
|
||||
'skip': (time) => controllers.playback().skip(time),
|
||||
'skipTo': (time) => controllers.playback().skipTo(time),
|
||||
'changePlaybackRate': (dir) => controllers.playback().changePlaybackRate(dir),
|
||||
'changeVolume': (delta) => controllers.playback().changeVolume(delta),
|
||||
'setVolume': (vol) => controllers.playback().setVolume(vol),
|
||||
'openItem': (infoHash, index) => controllers.playback().openItem(infoHash, index),
|
||||
|
||||
// Subtitles
|
||||
'openSubtitles': () => controllers.subtitles.openSubtitles(),
|
||||
'selectSubtitle': (index) => controllers.subtitles.selectSubtitle(index),
|
||||
'toggleSubtitlesMenu': () => controllers.subtitles.toggleSubtitlesMenu(),
|
||||
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),
|
||||
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect),
|
||||
'openSubtitles': () => controllers.subtitles().openSubtitles(),
|
||||
'selectSubtitle': (index) => controllers.subtitles().selectSubtitle(index),
|
||||
'toggleSubtitlesMenu': () => controllers.subtitles().toggleSubtitlesMenu(),
|
||||
'checkForSubtitles': () => controllers.subtitles().checkForSubtitles(),
|
||||
'addSubtitles': (files, autoSelect) => controllers.subtitles().addSubtitles(files, autoSelect),
|
||||
|
||||
// Local media: <video>, <audio>, external players
|
||||
'mediaStalled': () => controllers.media.mediaStalled(),
|
||||
'mediaError': (err) => controllers.media.mediaError(err),
|
||||
'mediaSuccess': () => controllers.media.mediaSuccess(),
|
||||
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
|
||||
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
|
||||
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
|
||||
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
|
||||
'mediaStalled': () => controllers.media().mediaStalled(),
|
||||
'mediaError': (err) => controllers.media().mediaError(err),
|
||||
'mediaSuccess': () => controllers.media().mediaSuccess(),
|
||||
'mediaTimeUpdate': () => controllers.media().mediaTimeUpdate(),
|
||||
'mediaMouseMoved': () => controllers.media().mediaMouseMoved(),
|
||||
'mediaControlsMouseEnter': () => controllers.media().controlsMouseEnter(),
|
||||
'mediaControlsMouseLeave': () => controllers.media().controlsMouseLeave(),
|
||||
'openExternalPlayer': () => controllers.media().openExternalPlayer(),
|
||||
'externalPlayerNotFound': () => controllers.media().externalPlayerNotFound(),
|
||||
|
||||
// Remote casting: Chromecast, Airplay, etc
|
||||
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
|
||||
@@ -213,13 +281,13 @@ const dispatchHandlers = {
|
||||
'stopCasting': () => lazyLoadCast().stop(),
|
||||
|
||||
// Preferences screen
|
||||
'preferences': () => controllers.prefs.show(),
|
||||
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
|
||||
'preferences': () => controllers.prefs().show(),
|
||||
'updatePreferences': (key, value) => controllers.prefs().update(key, value),
|
||||
'checkDownloadPath': checkDownloadPath,
|
||||
|
||||
// Update (check for new versions on Linux, where there's no auto updater)
|
||||
'updateAvailable': (version) => controllers.update.updateAvailable(version),
|
||||
'skipVersion': (version) => controllers.update.skipVersion(version),
|
||||
'updateAvailable': (version) => controllers.update().updateAvailable(version),
|
||||
'skipVersion': (version) => controllers.update().skipVersion(version),
|
||||
|
||||
// Navigation between screens (back, forward, ESC, etc)
|
||||
'exitModal': () => { state.modal = null },
|
||||
@@ -239,8 +307,8 @@ const dispatchHandlers = {
|
||||
'onOpen': onOpen,
|
||||
'error': onError,
|
||||
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
|
||||
'saveState': () => State.save(state),
|
||||
'saveStateThrottled': () => State.saveThrottled(state),
|
||||
'stateSave': () => State.save(state),
|
||||
'stateSaveImmediate': () => State.saveImmediate(state),
|
||||
'update': () => {} // No-op, just trigger an update
|
||||
}
|
||||
|
||||
@@ -251,13 +319,13 @@ function dispatch (action, ...args) {
|
||||
console.log('dispatch: %s %o', action, args)
|
||||
}
|
||||
|
||||
var handler = dispatchHandlers[action]
|
||||
const handler = dispatchHandlers[action]
|
||||
if (handler) handler(...args)
|
||||
else console.error('Missing dispatch handler: ' + action)
|
||||
|
||||
// Update the virtual DOM, unless it's just a mouse move event
|
||||
if (action !== 'mediaMouseMoved' ||
|
||||
controllers.playback.showOrHidePlayerControls()) {
|
||||
controllers.playback().showOrHidePlayerControls()) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
@@ -270,8 +338,9 @@ function setupIpc () {
|
||||
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
|
||||
|
||||
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
|
||||
ipcRenderer.on('windowBoundsChanged', onWindowBoundsChanged)
|
||||
|
||||
var tc = controllers.torrent
|
||||
const tc = controllers.torrent()
|
||||
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
|
||||
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
|
||||
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
|
||||
@@ -289,7 +358,7 @@ function setupIpc () {
|
||||
|
||||
ipcRenderer.send('ipcReady')
|
||||
|
||||
State.on('savedState', () => ipcRenderer.send('savedState'))
|
||||
State.on('stateSaved', () => ipcRenderer.send('stateSaved'))
|
||||
}
|
||||
|
||||
// Quits any modal popovers and returns to the torrent list screen
|
||||
@@ -298,7 +367,7 @@ function backToList () {
|
||||
state.modal = null
|
||||
state.location.backToFirst(function () {
|
||||
// If we were already on the torrent list, scroll to the top
|
||||
var contentTag = document.querySelector('.content')
|
||||
const contentTag = document.querySelector('.content')
|
||||
if (contentTag) contentTag.scrollTop = 0
|
||||
})
|
||||
}
|
||||
@@ -324,7 +393,7 @@ function resumeTorrents () {
|
||||
return torrentSummary
|
||||
})
|
||||
.filter((s) => s.status !== 'paused')
|
||||
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey))
|
||||
.forEach((s) => controllers.torrentList().startTorrentingSummary(s.torrentKey))
|
||||
}
|
||||
|
||||
// Set window dimensions to match video dimensions or fill the screen
|
||||
@@ -345,18 +414,18 @@ function setDimensions (dimensions) {
|
||||
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
|
||||
|
||||
// Limit window size to screen size
|
||||
var screenWidth = window.screen.width
|
||||
var screenHeight = window.screen.height
|
||||
var aspectRatio = dimensions.width / dimensions.height
|
||||
var scaleFactor = Math.min(
|
||||
const screenWidth = window.screen.width
|
||||
const screenHeight = window.screen.height
|
||||
const aspectRatio = dimensions.width / dimensions.height
|
||||
const scaleFactor = Math.min(
|
||||
Math.min(screenWidth / dimensions.width, 1),
|
||||
Math.min(screenHeight / dimensions.height, 1)
|
||||
)
|
||||
var width = Math.max(
|
||||
const width = Math.max(
|
||||
Math.floor(dimensions.width * scaleFactor),
|
||||
config.WINDOW_MIN_WIDTH
|
||||
)
|
||||
var height = Math.max(
|
||||
const height = Math.max(
|
||||
Math.floor(dimensions.height * scaleFactor),
|
||||
config.WINDOW_MIN_HEIGHT
|
||||
)
|
||||
@@ -371,22 +440,22 @@ function setDimensions (dimensions) {
|
||||
function onOpen (files) {
|
||||
if (!Array.isArray(files)) files = [ files ]
|
||||
|
||||
var url = state.location.url()
|
||||
var allTorrents = files.every(TorrentPlayer.isTorrent)
|
||||
var allSubtitles = files.every(controllers.subtitles.isSubtitle)
|
||||
const url = state.location.url()
|
||||
const allTorrents = files.every(TorrentPlayer.isTorrent)
|
||||
const allSubtitles = files.every(controllers.subtitles().isSubtitle)
|
||||
|
||||
if (allTorrents) {
|
||||
// Drop torrents onto the app: go to home screen, add torrents, no matter what
|
||||
dispatch('backToList')
|
||||
// All .torrent files? Add them.
|
||||
files.forEach((file) => controllers.torrentList.addTorrent(file))
|
||||
files.forEach((file) => controllers.torrentList().addTorrent(file))
|
||||
} else if (url === 'player' && allSubtitles) {
|
||||
// Drop subtitles onto a playing video: add subtitles
|
||||
controllers.subtitles.addSubtitles(files, true)
|
||||
controllers.subtitles().addSubtitles(files, true)
|
||||
} else if (url === 'home') {
|
||||
// Drop files onto home screen: show Create Torrent
|
||||
state.modal = null
|
||||
controllers.torrentList.showCreateTorrent(files)
|
||||
controllers.torrentList().showCreateTorrent(files)
|
||||
} else {
|
||||
// Drop files onto any other screen: show error
|
||||
return onError('Please go back to the torrent list before creating a new torrent.')
|
||||
@@ -408,13 +477,7 @@ function onError (err) {
|
||||
|
||||
function onPaste (e) {
|
||||
if (e.target.tagName.toLowerCase() === 'input') return
|
||||
|
||||
var torrentIds = electron.clipboard.readText().split('\n')
|
||||
torrentIds.forEach(function (torrentId) {
|
||||
torrentId = torrentId.trim()
|
||||
if (torrentId.length === 0) return
|
||||
controllers.torrentList.addTorrent(torrentId)
|
||||
})
|
||||
controllers.torrentList().addTorrent(electron.clipboard.readText())
|
||||
|
||||
update()
|
||||
}
|
||||
@@ -444,6 +507,13 @@ function onFullscreenChanged (e, isFullScreen) {
|
||||
update()
|
||||
}
|
||||
|
||||
function onWindowBoundsChanged (e, newBounds) {
|
||||
if (state.location.url() !== 'player') {
|
||||
state.saved.bounds = newBounds
|
||||
dispatch('stateSave')
|
||||
}
|
||||
}
|
||||
|
||||
function checkDownloadPath () {
|
||||
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
|
||||
if (err) {
|
||||
|
||||
130
src/renderer/pages/app.js
Normal file
@@ -0,0 +1,130 @@
|
||||
const colors = require('material-ui/styles/colors')
|
||||
const createGetter = require('fn-getter')
|
||||
const React = require('react')
|
||||
|
||||
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
|
||||
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
|
||||
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
|
||||
|
||||
const Header = require('../components/header')
|
||||
|
||||
// Perf optimization: Needed immediately, so do not lazy load it below
|
||||
const TorrentListPage = require('./torrent-list-page')
|
||||
|
||||
const Views = {
|
||||
'home': createGetter(() => TorrentListPage),
|
||||
'player': createGetter(() => require('./player-page')),
|
||||
'create-torrent': createGetter(() => require('./create-torrent-page')),
|
||||
'preferences': createGetter(() => require('./preferences-page'))
|
||||
}
|
||||
|
||||
const Modals = {
|
||||
'open-torrent-address-modal': createGetter(
|
||||
() => require('../components/open-torrent-address-modal')
|
||||
),
|
||||
'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')),
|
||||
'update-available-modal': createGetter(() => require('../components/update-available-modal')),
|
||||
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
|
||||
}
|
||||
|
||||
const fontFamily = process.platform === 'win32'
|
||||
? '"Segoe UI", sans-serif'
|
||||
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
|
||||
|
||||
darkBaseTheme.fontFamily = fontFamily
|
||||
darkBaseTheme.userAgent = false
|
||||
darkBaseTheme.palette.primary1Color = colors.grey50
|
||||
darkBaseTheme.palette.primary2Color = colors.grey50
|
||||
darkBaseTheme.palette.primary3Color = colors.grey600
|
||||
darkBaseTheme.palette.accent1Color = colors.redA200
|
||||
darkBaseTheme.palette.accent2Color = colors.redA400
|
||||
darkBaseTheme.palette.accent3Color = colors.redA100
|
||||
|
||||
let darkMuiTheme
|
||||
let lightMuiTheme
|
||||
|
||||
class App extends React.Component {
|
||||
render () {
|
||||
const state = this.props.state
|
||||
|
||||
// Hide player controls while playing video, if the mouse stays still for a while
|
||||
// Never hide the controls when:
|
||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||
// * The video is paused
|
||||
// * The video is playing remotely on Chromecast or Airplay
|
||||
const hideControls = state.shouldHidePlayerControls()
|
||||
|
||||
const cls = [
|
||||
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
||||
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
||||
]
|
||||
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
||||
if (state.window.isFocused) cls.push('is-focused')
|
||||
if (hideControls) cls.push('hide-video-controls')
|
||||
|
||||
if (!darkMuiTheme) {
|
||||
darkMuiTheme = getMuiTheme(darkBaseTheme)
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiThemeProvider muiTheme={darkMuiTheme}>
|
||||
<div className={'app ' + cls.join(' ')}>
|
||||
<Header state={state} />
|
||||
{this.getErrorPopover()}
|
||||
<div key='content' className='content'>{this.getView()}</div>
|
||||
{this.getModal()}
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
getErrorPopover () {
|
||||
const state = this.props.state
|
||||
const now = new Date().getTime()
|
||||
const recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
||||
const hasErrors = recentErrors.length > 0
|
||||
|
||||
const errorElems = recentErrors.map(function (error, i) {
|
||||
return (<div key={i} className='error'>{error.message}</div>)
|
||||
})
|
||||
return (
|
||||
<div key='errors'
|
||||
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
|
||||
<div key='title' className='title'>Error</div>
|
||||
{errorElems}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getModal () {
|
||||
const state = this.props.state
|
||||
if (!state.modal) return
|
||||
|
||||
if (!lightMuiTheme) {
|
||||
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
|
||||
lightBaseTheme.fontFamily = fontFamily
|
||||
lightBaseTheme.userAgent = false
|
||||
lightMuiTheme = getMuiTheme(lightBaseTheme)
|
||||
}
|
||||
|
||||
const ModalContents = Modals[state.modal.id]()
|
||||
return (
|
||||
<MuiThemeProvider muiTheme={lightMuiTheme}>
|
||||
<div key='modal' className='modal'>
|
||||
<div key='modal-background' className='modal-background' />
|
||||
<div key='modal-content' className='modal-content'>
|
||||
<ModalContents state={state} />
|
||||
</div>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
getView () {
|
||||
const state = this.props.state
|
||||
const View = Views[state.location.url()]()
|
||||
return (<View state={state} />)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = App
|
||||
216
src/renderer/pages/create-torrent-page.js
Normal file
@@ -0,0 +1,216 @@
|
||||
const createTorrent = require('create-torrent')
|
||||
const path = require('path')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
const React = require('react')
|
||||
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
const FlatButton = require('material-ui/FlatButton').default
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
const TextField = require('material-ui/TextField').default
|
||||
const Checkbox = require('material-ui/Checkbox').default
|
||||
|
||||
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
|
||||
const Heading = require('../components/heading')
|
||||
const ShowMore = require('../components/show-more')
|
||||
|
||||
// Shows a basic UI to create a torrent, from an already-selected file or folder.
|
||||
// Includes a "Show Advanced..." button and more advanced UI.
|
||||
class CreateTorrentPage extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const state = this.props.state
|
||||
const info = state.location.current()
|
||||
|
||||
// First, extract the base folder that the files are all in
|
||||
let pathPrefix = info.folderPath
|
||||
if (!pathPrefix) {
|
||||
pathPrefix = info.files.map((x) => x.path).reduce(findCommonPrefix)
|
||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||
pathPrefix = path.dirname(pathPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
// Then, exclude .DS_Store and other dotfiles
|
||||
const files = info.files
|
||||
.filter((f) => !containsDots(f.path, pathPrefix))
|
||||
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
||||
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
|
||||
|
||||
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
||||
// as the default name. Show all files relative to the base folder.
|
||||
let defaultName, basePath
|
||||
if (files.length === 1) {
|
||||
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
|
||||
defaultName = files[0].name
|
||||
basePath = pathPrefix
|
||||
} else {
|
||||
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a'
|
||||
defaultName = path.basename(pathPrefix)
|
||||
basePath = path.dirname(pathPrefix)
|
||||
}
|
||||
|
||||
// Default trackers
|
||||
const trackers = createTorrent.announceList.join('\n')
|
||||
|
||||
this.state = {
|
||||
comment: '',
|
||||
isPrivate: false,
|
||||
pathPrefix,
|
||||
basePath,
|
||||
defaultName,
|
||||
files,
|
||||
trackers
|
||||
}
|
||||
|
||||
// Create React event handlers only once
|
||||
this.setIsPrivate = (_, isPrivate) => this.setState({isPrivate})
|
||||
this.setComment = (_, comment) => this.setState({comment})
|
||||
this.setTrackers = (_, trackers) => this.setState({trackers})
|
||||
this.handleSubmit = handleSubmit.bind(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
const files = this.state.files
|
||||
|
||||
// Sanity check: show the number of files and total size
|
||||
const numFiles = files.length
|
||||
const totalBytes = files
|
||||
.map((f) => f.size)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
const torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
|
||||
|
||||
return (
|
||||
<div className='create-torrent'>
|
||||
<Heading level={1}>Create torrent {this.state.defaultName}</Heading>
|
||||
<div className='torrent-info'>{torrentInfo}</div>
|
||||
<div className='torrent-attribute'>
|
||||
<label>Path:</label>
|
||||
<div>{this.state.pathPrefix}</div>
|
||||
</div>
|
||||
<ShowMore
|
||||
style={{
|
||||
marginBottom: 10
|
||||
}}
|
||||
hideLabel='Hide advanced settings...'
|
||||
showLabel='Show advanced settings...'>
|
||||
{this.renderAdvanced()}
|
||||
</ShowMore>
|
||||
<div className='float-right'>
|
||||
<FlatButton
|
||||
className='control cancel'
|
||||
label='Cancel'
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
onClick={dispatcher('cancel')} />
|
||||
<RaisedButton
|
||||
className='control create-torrent-button'
|
||||
label='Create Torrent'
|
||||
primary
|
||||
onClick={this.handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Renders everything after clicking Show Advanced...:
|
||||
// * Is Private? (private torrents, not announced to DHT)
|
||||
// * Announce list (trackers)
|
||||
// * Comment
|
||||
renderAdvanced () {
|
||||
// Create file list
|
||||
const maxFileElems = 100
|
||||
const files = this.state.files
|
||||
const fileElems = files.slice(0, maxFileElems).map((file, i) => {
|
||||
const relativePath = path.relative(this.state.pathPrefix, file.path)
|
||||
return (<div key={i}>{relativePath}</div>)
|
||||
})
|
||||
if (files.length > maxFileElems) {
|
||||
fileElems.push(<div key='more'>+ {files.length - maxFileElems} more</div>)
|
||||
}
|
||||
|
||||
// Align the text fields
|
||||
const textFieldStyle = { width: '' }
|
||||
const textareaStyle = { margin: 0 }
|
||||
|
||||
return (
|
||||
<div key='advanced' className='create-torrent-advanced'>
|
||||
<div key='private' className='torrent-attribute'>
|
||||
<label>Private:</label>
|
||||
<Checkbox
|
||||
className='torrent-is-private control'
|
||||
style={{display: ''}}
|
||||
checked={this.state.isPrivate}
|
||||
onCheck={this.setIsPrivate} />
|
||||
</div>
|
||||
<div key='trackers' className='torrent-attribute'>
|
||||
<label>Trackers:</label>
|
||||
<TextField
|
||||
className='torrent-trackers control'
|
||||
style={textFieldStyle}
|
||||
textareaStyle={textareaStyle}
|
||||
multiLine
|
||||
rows={2}
|
||||
rowsMax={10}
|
||||
value={this.state.trackers}
|
||||
onChange={this.setTrackers} />
|
||||
</div>
|
||||
<div key='comment' className='torrent-attribute'>
|
||||
<label>Comment:</label>
|
||||
<TextField
|
||||
className='torrent-comment control'
|
||||
style={textFieldStyle}
|
||||
textareaStyle={textareaStyle}
|
||||
hintText='Optionally describe your torrent...'
|
||||
multiLine
|
||||
rows={2}
|
||||
rowsMax={10}
|
||||
value={this.state.comment}
|
||||
onChange={this.setComment} />
|
||||
</div>
|
||||
<div key='files' className='torrent-attribute'>
|
||||
<label>Files:</label>
|
||||
<div>{fileElems}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit () {
|
||||
const announceList = this.state.trackers
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== '')
|
||||
const options = {
|
||||
// We can't let the user choose their own name if we want WebTorrent
|
||||
// to use the files in place rather than creating a new folder.
|
||||
name: this.state.defaultName,
|
||||
path: this.state.basePath,
|
||||
files: this.state.files,
|
||||
announce: announceList,
|
||||
private: this.state.isPrivate,
|
||||
comment: this.state.comment.trim()
|
||||
}
|
||||
dispatch('createTorrent', options)
|
||||
}
|
||||
|
||||
// Finds the longest common prefix
|
||||
function findCommonPrefix (a, b) {
|
||||
let i
|
||||
for (i = 0; i < a.length && i < b.length; i++) {
|
||||
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
||||
}
|
||||
if (i === a.length) return a
|
||||
if (i === b.length) return b
|
||||
return a.substring(0, i)
|
||||
}
|
||||
|
||||
function containsDots (path, pathPrefix) {
|
||||
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
|
||||
return ('/' + suffix).includes('/.')
|
||||
}
|
||||
|
||||
module.exports = CreateTorrentPage
|
||||
@@ -2,28 +2,39 @@ const React = require('react')
|
||||
const Bitfield = require('bitfield')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
const zeroFill = require('zero-fill')
|
||||
const path = require('path')
|
||||
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const Playlist = require('../lib/playlist')
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
const config = require('../../config')
|
||||
|
||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||
module.exports = class Player extends React.Component {
|
||||
render () {
|
||||
// Show the video as large as will fit in the window, play immediately
|
||||
// If the video is on Chromecast or Airplay, show a title screen instead
|
||||
var state = this.props.state
|
||||
var showVideo = state.playing.location === 'local'
|
||||
const state = this.props.state
|
||||
const showVideo = state.playing.location === 'local'
|
||||
const showControls = state.playing.location !== 'external'
|
||||
return (
|
||||
<div
|
||||
className='player'
|
||||
onWheel={handleVolumeWheel}
|
||||
onMouseMove={dispatcher('mediaMouseMoved')}>
|
||||
{showVideo ? renderMedia(state) : renderCastScreen(state)}
|
||||
{renderPlayerControls(state)}
|
||||
{showControls ? renderPlayerControls(state) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onComponentWillUnmount () {
|
||||
// Unload the media element so that Chromium stops trying to fetch data
|
||||
const tag = document.querySelector('audio,video')
|
||||
if (!tag) return
|
||||
tag.pause()
|
||||
tag.src = ''
|
||||
tag.load()
|
||||
}
|
||||
}
|
||||
|
||||
// Handles volume change by wheel
|
||||
@@ -37,7 +48,7 @@ function renderMedia (state) {
|
||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||
// Instead, grab the DOM node and play/pause it if necessary
|
||||
// Get the <video> or <audio> tag
|
||||
var mediaElement = document.querySelector(state.playing.type)
|
||||
const mediaElement = document.querySelector(state.playing.type)
|
||||
if (mediaElement !== null) {
|
||||
if (state.playing.isPaused && !mediaElement.paused) {
|
||||
mediaElement.pause()
|
||||
@@ -65,20 +76,20 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
// Switch to the newly added subtitle track, if available
|
||||
var tracks = mediaElement.textTracks || []
|
||||
for (var j = 0; j < tracks.length; j++) {
|
||||
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
||||
const tracks = mediaElement.textTracks || []
|
||||
for (let j = 0; j < tracks.length; j++) {
|
||||
const isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
||||
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
|
||||
}
|
||||
|
||||
// Save video position
|
||||
var file = state.getPlayingFileSummary()
|
||||
const file = state.getPlayingFileSummary()
|
||||
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
||||
file.duration = state.playing.duration = mediaElement.duration
|
||||
|
||||
// Save selected subtitle
|
||||
if (state.playing.subtitles.selectedIndex !== -1) {
|
||||
var index = state.playing.subtitles.selectedIndex
|
||||
const index = state.playing.subtitles.selectedIndex
|
||||
file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath
|
||||
} else if (file.selectedSubtitle != null) {
|
||||
delete file.selectedSubtitle
|
||||
@@ -88,11 +99,11 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
// Add subtitles to the <video> tag
|
||||
var trackTags = []
|
||||
const trackTags = []
|
||||
if (state.playing.subtitles.selectedIndex >= 0) {
|
||||
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||
var track = state.playing.subtitles.tracks[i]
|
||||
var isSelected = state.playing.subtitles.selectedIndex === i
|
||||
for (let i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||
const track = state.playing.subtitles.tracks[i]
|
||||
const isSelected = state.playing.subtitles.selectedIndex === i
|
||||
trackTags.push(
|
||||
<track
|
||||
key={i}
|
||||
@@ -105,10 +116,10 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
// Create the <audio> or <video> tag
|
||||
var MediaTagName = state.playing.type
|
||||
var mediaTag = (
|
||||
const MediaTagName = state.playing.type
|
||||
const mediaTag = (
|
||||
<MediaTagName
|
||||
src={state.server.localURL}
|
||||
src={Playlist.getCurrentLocalURL(state)}
|
||||
onDoubleClick={dispatcher('toggleFullScreen')}
|
||||
onLoadedMetadata={onLoadedMetadata}
|
||||
onEnded={onEnded}
|
||||
@@ -135,21 +146,25 @@ function renderMedia (state) {
|
||||
// As soon as we know the video dimensions, resize the window
|
||||
function onLoadedMetadata (e) {
|
||||
if (state.playing.type !== 'video') return
|
||||
var video = e.target
|
||||
var dimensions = {
|
||||
const video = e.target
|
||||
const dimensions = {
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight
|
||||
}
|
||||
dispatch('setDimensions', dimensions)
|
||||
}
|
||||
|
||||
// When the video completes, pause the video instead of looping
|
||||
function onEnded (e) {
|
||||
state.playing.isPaused = true
|
||||
if (Playlist.hasNext(state)) {
|
||||
dispatch('nextTrack')
|
||||
} else {
|
||||
// When the last video completes, pause the video instead of looping
|
||||
state.playing.isPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
function onCanPlay (e) {
|
||||
var elem = e.target
|
||||
const elem = e.target
|
||||
if (state.playing.type === 'video' &&
|
||||
elem.webkitVideoDecodedByteCount === 0) {
|
||||
dispatch('mediaError', 'Video codec unsupported')
|
||||
@@ -163,15 +178,15 @@ function renderMedia (state) {
|
||||
}
|
||||
|
||||
function renderOverlay (state) {
|
||||
var elems = []
|
||||
var audioMetadataElem = renderAudioMetadata(state)
|
||||
var spinnerElem = renderLoadingSpinner(state)
|
||||
const elems = []
|
||||
const audioMetadataElem = renderAudioMetadata(state)
|
||||
const spinnerElem = renderLoadingSpinner(state)
|
||||
if (audioMetadataElem) elems.push(audioMetadataElem)
|
||||
if (spinnerElem) elems.push(spinnerElem)
|
||||
|
||||
// Video fills the window, centered with black bars if necessary
|
||||
// Audio gets a static poster image and a summary of the file metadata.
|
||||
var style
|
||||
let style
|
||||
if (state.playing.type === 'audio') {
|
||||
style = { backgroundImage: cssBackgroundImagePoster(state) }
|
||||
} else if (elems.length !== 0) {
|
||||
@@ -189,27 +204,27 @@ function renderOverlay (state) {
|
||||
}
|
||||
|
||||
function renderAudioMetadata (state) {
|
||||
var fileSummary = state.getPlayingFileSummary()
|
||||
const fileSummary = state.getPlayingFileSummary()
|
||||
if (!fileSummary.audioInfo) return
|
||||
var info = fileSummary.audioInfo
|
||||
const info = fileSummary.audioInfo
|
||||
|
||||
// Get audio track info
|
||||
var title = info.title
|
||||
let title = info.title
|
||||
if (!title) {
|
||||
title = fileSummary.name
|
||||
}
|
||||
var artist = info.artist && info.artist[0]
|
||||
var album = info.album
|
||||
let artist = info.artist && info.artist[0]
|
||||
let album = info.album
|
||||
if (album && info.year && !album.includes(info.year)) {
|
||||
album += ' (' + info.year + ')'
|
||||
}
|
||||
var track
|
||||
let track
|
||||
if (info.track && info.track.no && info.track.of) {
|
||||
track = info.track.no + ' of ' + info.track.of
|
||||
}
|
||||
|
||||
// Show a small info box in the middle of the screen with title/album/etc
|
||||
var elems = []
|
||||
const elems = []
|
||||
if (artist) {
|
||||
elems.push((
|
||||
<div key='artist' className='audio-artist'>
|
||||
@@ -233,7 +248,7 @@ function renderAudioMetadata (state) {
|
||||
}
|
||||
|
||||
// Align the title with the other info, if available. Otherwise, center title
|
||||
var emptyLabel = (<label />)
|
||||
const emptyLabel = (<label />)
|
||||
elems.unshift((
|
||||
<div key='title' className='audio-title'>
|
||||
{elems.length ? emptyLabel : undefined}{title}
|
||||
@@ -245,14 +260,14 @@ function renderAudioMetadata (state) {
|
||||
|
||||
function renderLoadingSpinner (state) {
|
||||
if (state.playing.isPaused) return
|
||||
var isProbablyStalled = state.playing.isStalled ||
|
||||
const isProbablyStalled = state.playing.isStalled ||
|
||||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
||||
if (!isProbablyStalled) return
|
||||
|
||||
var prog = state.getPlayingTorrentSummary().progress || {}
|
||||
var fileProgress = 0
|
||||
const prog = state.getPlayingTorrentSummary().progress || {}
|
||||
let fileProgress = 0
|
||||
if (prog.files) {
|
||||
var file = prog.files[state.playing.fileIndex]
|
||||
const file = prog.files[state.playing.fileIndex]
|
||||
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
|
||||
}
|
||||
|
||||
@@ -269,7 +284,7 @@ function renderLoadingSpinner (state) {
|
||||
}
|
||||
|
||||
function renderCastScreen (state) {
|
||||
var castIcon, castType, isCast
|
||||
let castIcon, castType, isCast
|
||||
if (state.playing.location.startsWith('chromecast')) {
|
||||
castIcon = 'cast_connected'
|
||||
castType = 'Chromecast'
|
||||
@@ -283,11 +298,8 @@ function renderCastScreen (state) {
|
||||
castType = 'DLNA'
|
||||
isCast = true
|
||||
} else if (state.playing.location === 'external') {
|
||||
// TODO: get the player name in a more reliable way
|
||||
var playerPath = state.saved.prefs.externalPlayerPath
|
||||
var playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
|
||||
castIcon = 'tv'
|
||||
castType = playerName
|
||||
castType = state.getExternalPlayerName()
|
||||
isCast = false
|
||||
} else if (state.playing.location === 'error') {
|
||||
castIcon = 'error_outline'
|
||||
@@ -295,15 +307,15 @@ function renderCastScreen (state) {
|
||||
isCast = false
|
||||
}
|
||||
|
||||
var isStarting = state.playing.location.endsWith('-pending')
|
||||
var castName = state.playing.castName
|
||||
var castStatus
|
||||
const isStarting = state.playing.location.endsWith('-pending')
|
||||
const castName = state.playing.castName
|
||||
let castStatus
|
||||
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
|
||||
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
|
||||
else castStatus = ''
|
||||
|
||||
// Show a nice title image, if possible
|
||||
var style = {
|
||||
const style = {
|
||||
backgroundImage: cssBackgroundImagePoster(state)
|
||||
}
|
||||
|
||||
@@ -321,12 +333,12 @@ function renderCastScreen (state) {
|
||||
function renderCastOptions (state) {
|
||||
if (!state.devices.castMenu) return
|
||||
|
||||
var {location, devices} = state.devices.castMenu
|
||||
var player = state.devices[location]
|
||||
const {location, devices} = state.devices.castMenu
|
||||
const player = state.devices[location]
|
||||
|
||||
var items = devices.map(function (device, ix) {
|
||||
var isSelected = player.device === device
|
||||
var name = device.name
|
||||
const items = devices.map(function (device, ix) {
|
||||
const isSelected = player.device === device
|
||||
const name = device.name
|
||||
return (
|
||||
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
|
||||
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||
@@ -343,11 +355,11 @@ function renderCastOptions (state) {
|
||||
}
|
||||
|
||||
function renderSubtitleOptions (state) {
|
||||
var subtitles = state.playing.subtitles
|
||||
const subtitles = state.playing.subtitles
|
||||
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||
|
||||
var items = subtitles.tracks.map(function (track, ix) {
|
||||
var isSelected = state.playing.subtitles.selectedIndex === ix
|
||||
const items = subtitles.tracks.map(function (track, ix) {
|
||||
const isSelected = state.playing.subtitles.selectedIndex === ix
|
||||
return (
|
||||
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
|
||||
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
||||
@@ -356,8 +368,8 @@ function renderSubtitleOptions (state) {
|
||||
)
|
||||
})
|
||||
|
||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||
const noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||
const noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||
return (
|
||||
<ul key='subtitle-options' className='options-list'>
|
||||
{items}
|
||||
@@ -370,32 +382,39 @@ function renderSubtitleOptions (state) {
|
||||
}
|
||||
|
||||
function renderPlayerControls (state) {
|
||||
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
||||
const positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||
const captionsClass = state.playing.subtitles.tracks.length === 0
|
||||
? 'disabled'
|
||||
: state.playing.subtitles.selectedIndex >= 0
|
||||
? 'active'
|
||||
: ''
|
||||
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
|
||||
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
|
||||
|
||||
var elements = [
|
||||
const elements = [
|
||||
<div key='playback-bar' className='playback-bar'>
|
||||
{renderLoadingBar(state)}
|
||||
<div
|
||||
key='cursor'
|
||||
className='playback-cursor'
|
||||
style={playbackCursorStyle}
|
||||
/>
|
||||
style={playbackCursorStyle} />
|
||||
<div
|
||||
key='scrub-bar'
|
||||
className='scrub-bar'
|
||||
draggable='true'
|
||||
onDragStart={handleDragStart}
|
||||
onClick={handleScrub}
|
||||
onDrag={handleScrub}
|
||||
/>
|
||||
onDrag={handleScrub} />
|
||||
</div>,
|
||||
|
||||
<i
|
||||
key='skip-previous'
|
||||
className={'icon skip-previous float-left ' + prevClass}
|
||||
onClick={dispatcher('previousTrack')}>
|
||||
skip_previous
|
||||
</i>,
|
||||
|
||||
<i
|
||||
key='play'
|
||||
className='icon play-pause float-left'
|
||||
@@ -403,6 +422,13 @@ function renderPlayerControls (state) {
|
||||
{state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||
</i>,
|
||||
|
||||
<i
|
||||
key='skip-next'
|
||||
className={'icon skip-next float-left ' + nextClass}
|
||||
onClick={dispatcher('nextTrack')}>
|
||||
skip_next
|
||||
</i>,
|
||||
|
||||
<i
|
||||
key='fullscreen'
|
||||
className='icon fullscreen float-right'
|
||||
@@ -412,7 +438,7 @@ function renderPlayerControls (state) {
|
||||
]
|
||||
|
||||
if (state.playing.type === 'video') {
|
||||
// show closed captions icon
|
||||
// Show closed captions icon
|
||||
elements.push((
|
||||
<i
|
||||
key='subtitles'
|
||||
@@ -424,24 +450,24 @@ function renderPlayerControls (state) {
|
||||
}
|
||||
|
||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||
var castTypes = ['chromecast', 'airplay', 'dlna']
|
||||
var isCastingAnywhere = castTypes.some(
|
||||
const castTypes = ['chromecast', 'airplay', 'dlna']
|
||||
const isCastingAnywhere = castTypes.some(
|
||||
(castType) => state.playing.location.startsWith(castType))
|
||||
|
||||
// Add the cast buttons. Icons for each cast type, connected/disconnected:
|
||||
var buttonIcons = {
|
||||
const buttonIcons = {
|
||||
'chromecast': {true: 'cast_connected', false: 'cast'},
|
||||
'airplay': {true: 'airplay', false: 'airplay'},
|
||||
'dlna': {true: 'tv', false: 'tv'}
|
||||
}
|
||||
castTypes.forEach(function (castType) {
|
||||
// Do we show this button (eg. the Chromecast button) at all?
|
||||
var isCasting = state.playing.location.startsWith(castType)
|
||||
var player = state.devices[castType]
|
||||
const isCasting = state.playing.location.startsWith(castType)
|
||||
const player = state.devices[castType]
|
||||
if ((!player || player.getDevices().length === 0) && !isCasting) return
|
||||
|
||||
// Show the button. Three options for eg the Chromecast button:
|
||||
var buttonClass, buttonHandler
|
||||
let buttonClass, buttonHandler
|
||||
if (isCasting) {
|
||||
// Option 1: we are currently connected to Chromecast. Button stops the cast.
|
||||
buttonClass = 'active'
|
||||
@@ -455,7 +481,7 @@ function renderPlayerControls (state) {
|
||||
buttonClass = ''
|
||||
buttonHandler = dispatcher('toggleCastMenu', castType)
|
||||
}
|
||||
var buttonIcon = buttonIcons[castType][isCasting]
|
||||
const buttonIcon = buttonIcons[castType][isCasting]
|
||||
|
||||
elements.push((
|
||||
<i
|
||||
@@ -468,20 +494,18 @@ function renderPlayerControls (state) {
|
||||
})
|
||||
|
||||
// Render volume slider
|
||||
var volume = state.playing.volume
|
||||
var volumeIcon = 'volume_' + (
|
||||
const volume = state.playing.volume
|
||||
const volumeIcon = 'volume_' + (
|
||||
volume === 0 ? 'off'
|
||||
: volume < 0.3 ? 'mute'
|
||||
: volume < 0.6 ? 'down'
|
||||
: 'up')
|
||||
var volumeStyle = {
|
||||
const volumeStyle = {
|
||||
background: '-webkit-gradient(linear, left top, right top, ' +
|
||||
'color-stop(' + (volume * 100) + '%, #eee), ' +
|
||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
||||
}
|
||||
|
||||
// TODO: dcposch change the range input to use value / onChanged instead of
|
||||
// "readonly" / onMouse[Down,Move,Up]
|
||||
elements.push((
|
||||
<div key='volume' className='volume float-left'>
|
||||
<i
|
||||
@@ -494,21 +518,20 @@ function renderPlayerControls (state) {
|
||||
type='range' min='0' max='1' step='0.05'
|
||||
value={volume}
|
||||
onChange={handleVolumeScrub}
|
||||
style={volumeStyle}
|
||||
/>
|
||||
style={volumeStyle} />
|
||||
</div>
|
||||
))
|
||||
|
||||
// Show video playback progress
|
||||
var currentTimeStr = formatTime(state.playing.currentTime)
|
||||
var durationStr = formatTime(state.playing.duration)
|
||||
const currentTimeStr = formatTime(state.playing.currentTime)
|
||||
const durationStr = formatTime(state.playing.duration)
|
||||
elements.push((
|
||||
<span key='time' className='time float-left'>
|
||||
{currentTimeStr} / {durationStr}
|
||||
</span>
|
||||
))
|
||||
|
||||
// render playback rate
|
||||
// Render playback rate
|
||||
if (state.playing.playbackRate !== 1) {
|
||||
elements.push((
|
||||
<span key='rate' className='rate float-left'>
|
||||
@@ -518,7 +541,9 @@ function renderPlayerControls (state) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div key='controls' className='controls'>
|
||||
<div key='controls' className='controls'
|
||||
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
|
||||
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
|
||||
{elements}
|
||||
{renderCastOptions(state)}
|
||||
{renderSubtitleOptions(state)}
|
||||
@@ -528,7 +553,7 @@ function renderPlayerControls (state) {
|
||||
function handleDragStart (e) {
|
||||
// Prevent the cursor from changing, eg to a green + icon on Mac
|
||||
if (e.dataTransfer) {
|
||||
var dt = e.dataTransfer
|
||||
const dt = e.dataTransfer
|
||||
dt.effectAllowed = 'none'
|
||||
}
|
||||
}
|
||||
@@ -537,9 +562,9 @@ function renderPlayerControls (state) {
|
||||
function handleScrub (e) {
|
||||
if (!e.clientX) return
|
||||
dispatch('mediaMouseMoved')
|
||||
var windowWidth = document.querySelector('body').clientWidth
|
||||
var fraction = e.clientX / windowWidth
|
||||
var position = fraction * state.playing.duration /* seconds */
|
||||
const windowWidth = document.querySelector('body').clientWidth
|
||||
const fraction = e.clientX / windowWidth
|
||||
const position = fraction * state.playing.duration /* seconds */
|
||||
dispatch('skipTo', position)
|
||||
}
|
||||
|
||||
@@ -570,18 +595,20 @@ function renderPlayerControls (state) {
|
||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||
// can be 'spongey' / non-contiguous
|
||||
function renderLoadingBar (state) {
|
||||
var torrentSummary = state.getPlayingTorrentSummary()
|
||||
if (config.IS_TEST) return // Don't integration test the loading bar. Screenshots won't match.
|
||||
|
||||
const torrentSummary = state.getPlayingTorrentSummary()
|
||||
if (!torrentSummary.progress) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Find all contiguous parts of the torrent which are loaded
|
||||
var prog = torrentSummary.progress
|
||||
var fileProg = prog.files[state.playing.fileIndex]
|
||||
var parts = []
|
||||
var lastPiecePresent = false
|
||||
for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
||||
var partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
|
||||
const prog = torrentSummary.progress
|
||||
const fileProg = prog.files[state.playing.fileIndex]
|
||||
const parts = []
|
||||
let lastPiecePresent = false
|
||||
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
||||
const partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
|
||||
if (partPresent && !lastPiecePresent) {
|
||||
parts.push({start: i - fileProg.startPiece, count: 1})
|
||||
} else if (partPresent) {
|
||||
@@ -591,8 +618,8 @@ function renderLoadingBar (state) {
|
||||
}
|
||||
|
||||
// Output some bars to show which parts of the file are loaded
|
||||
var loadingBarElems = parts.map(function (part, i) {
|
||||
var style = {
|
||||
const loadingBarElems = parts.map(function (part, i) {
|
||||
const style = {
|
||||
left: (100 * part.start / fileProg.numPieces) + '%',
|
||||
width: (100 * part.count / fileProg.numPieces) + '%'
|
||||
}
|
||||
@@ -604,10 +631,10 @@ function renderLoadingBar (state) {
|
||||
|
||||
// Returns the CSS background-image string for a poster image + dark vignette
|
||||
function cssBackgroundImagePoster (state) {
|
||||
var torrentSummary = state.getPlayingTorrentSummary()
|
||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||
const torrentSummary = state.getPlayingTorrentSummary()
|
||||
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||
if (!posterPath) return ''
|
||||
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
||||
return cssBackgroundImageDarkGradient() + `, url('${posterPath}')`
|
||||
}
|
||||
|
||||
function cssBackgroundImageDarkGradient () {
|
||||
@@ -620,12 +647,12 @@ function formatTime (time) {
|
||||
return '0:00'
|
||||
}
|
||||
|
||||
var hours = Math.floor(time / 3600)
|
||||
var minutes = Math.floor(time % 3600 / 60)
|
||||
let hours = Math.floor(time / 3600)
|
||||
let minutes = Math.floor(time % 3600 / 60)
|
||||
if (hours > 0) {
|
||||
minutes = zeroFill(2, minutes)
|
||||
}
|
||||
var seconds = zeroFill(2, Math.floor(time % 60))
|
||||
let seconds = zeroFill(2, Math.floor(time % 60))
|
||||
|
||||
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
|
||||
}
|
||||
192
src/renderer/pages/preferences-page.js
Normal file
@@ -0,0 +1,192 @@
|
||||
const path = require('path')
|
||||
const React = require('react')
|
||||
|
||||
const colors = require('material-ui/styles/colors')
|
||||
const Checkbox = require('material-ui/Checkbox').default
|
||||
const RaisedButton = require('material-ui/RaisedButton').default
|
||||
const Heading = require('../components/heading')
|
||||
const PathSelector = require('../components/path-selector')
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
const config = require('../../config')
|
||||
|
||||
class PreferencesPage extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.handleDownloadPathChange =
|
||||
this.handleDownloadPathChange.bind(this)
|
||||
|
||||
this.handleOpenExternalPlayerChange =
|
||||
this.handleOpenExternalPlayerChange.bind(this)
|
||||
|
||||
this.handleExternalPlayerPathChange =
|
||||
this.handleExternalPlayerPathChange.bind(this)
|
||||
|
||||
this.handleStartupChange =
|
||||
this.handleStartupChange.bind(this)
|
||||
}
|
||||
|
||||
downloadPathSelector () {
|
||||
return (
|
||||
<Preference>
|
||||
<PathSelector
|
||||
dialog={{
|
||||
title: 'Select download directory',
|
||||
properties: [ 'openDirectory' ]
|
||||
}}
|
||||
onChange={this.handleDownloadPathChange}
|
||||
title='Download location'
|
||||
value={this.props.state.unsaved.prefs.downloadPath} />
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
|
||||
handleDownloadPathChange (filePath) {
|
||||
dispatch('updatePreferences', 'downloadPath', filePath)
|
||||
}
|
||||
|
||||
openExternalPlayerCheckbox () {
|
||||
return (
|
||||
<Preference>
|
||||
<Checkbox
|
||||
className='control'
|
||||
checked={!this.props.state.unsaved.prefs.openExternalPlayer}
|
||||
label={'Play torrent media files using WebTorrent'}
|
||||
onCheck={this.handleOpenExternalPlayerChange} />
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
|
||||
handleOpenExternalPlayerChange (e, isChecked) {
|
||||
dispatch('updatePreferences', 'openExternalPlayer', !isChecked)
|
||||
}
|
||||
|
||||
externalPlayerPathSelector () {
|
||||
const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
|
||||
const playerName = this.props.state.getExternalPlayerName()
|
||||
|
||||
const description = this.props.state.unsaved.prefs.openExternalPlayer
|
||||
? `Torrent media files will always play in ${playerName}.`
|
||||
: `Torrent media files will play in ${playerName} if WebTorrent cannot play them.`
|
||||
|
||||
return (
|
||||
<Preference>
|
||||
<p>{description}</p>
|
||||
<PathSelector
|
||||
dialog={{
|
||||
title: 'Select media player app',
|
||||
properties: [ 'openFile' ]
|
||||
}}
|
||||
displayValue={playerName}
|
||||
onChange={this.handleExternalPlayerPathChange}
|
||||
title='External player'
|
||||
value={playerPath ? path.dirname(playerPath) : null} />
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
|
||||
handleExternalPlayerPathChange (filePath) {
|
||||
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
||||
}
|
||||
|
||||
setDefaultAppButton () {
|
||||
const isFileHandler = this.props.state.unsaved.prefs.isFileHandler
|
||||
if (isFileHandler) {
|
||||
return (
|
||||
<Preference>
|
||||
<p>WebTorrent is your default torrent app. Hooray!</p>
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Preference>
|
||||
<p>WebTorrent is not currently the default torrent app.</p>
|
||||
<RaisedButton
|
||||
className='control'
|
||||
onClick={this.handleSetDefaultApp}
|
||||
label='Make WebTorrent the default' />
|
||||
</Preference>
|
||||
)
|
||||
}
|
||||
|
||||
handleStartupChange (e, isChecked) {
|
||||
dispatch('updatePreferences', 'startup', isChecked)
|
||||
}
|
||||
|
||||
setStartupSection () {
|
||||
if (config.IS_PORTABLE) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesSection title='Startup'>
|
||||
<Preference>
|
||||
<Checkbox
|
||||
className='control'
|
||||
checked={this.props.state.unsaved.prefs.startup}
|
||||
label={'Open WebTorrent on startup.'}
|
||||
onCheck={this.handleStartupChange}
|
||||
/>
|
||||
</Preference>
|
||||
</PreferencesSection>
|
||||
)
|
||||
}
|
||||
|
||||
handleSetDefaultApp () {
|
||||
dispatch('updatePreferences', 'isFileHandler', true)
|
||||
}
|
||||
|
||||
render () {
|
||||
const style = {
|
||||
color: colors.grey400,
|
||||
marginLeft: 25,
|
||||
marginRight: 25
|
||||
}
|
||||
return (
|
||||
<div style={style}>
|
||||
<PreferencesSection title='Downloads'>
|
||||
{this.downloadPathSelector()}
|
||||
</PreferencesSection>
|
||||
<PreferencesSection title='Playback'>
|
||||
{this.openExternalPlayerCheckbox()}
|
||||
{this.externalPlayerPathSelector()}
|
||||
</PreferencesSection>
|
||||
<PreferencesSection title='Default torrent app'>
|
||||
{this.setDefaultAppButton()}
|
||||
</PreferencesSection>
|
||||
{this.setStartupSection()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class PreferencesSection extends React.Component {
|
||||
static get propTypes () {
|
||||
return {
|
||||
title: React.PropTypes.string
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const style = {
|
||||
marginBottom: 25,
|
||||
marginTop: 25
|
||||
}
|
||||
return (
|
||||
<div style={style}>
|
||||
<Heading level={2}>{this.props.title}</Heading>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Preference extends React.Component {
|
||||
render () {
|
||||
const style = { marginBottom: 10 }
|
||||
return (<div style={style}>{this.props.children}</div>)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PreferencesPage
|
||||
@@ -1,15 +1,18 @@
|
||||
const React = require('react')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
|
||||
const Checkbox = require('material-ui/Checkbox').default
|
||||
const LinearProgress = require('material-ui/LinearProgress').default
|
||||
|
||||
const TorrentSummary = require('../lib/torrent-summary')
|
||||
const TorrentPlayer = require('../lib/torrent-player')
|
||||
const {dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class TorrentList extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
const state = this.props.state
|
||||
|
||||
var contents = []
|
||||
const contents = []
|
||||
if (state.downloadPathStatus === 'missing') {
|
||||
contents.push(
|
||||
<div key='torrent-missing-path'>
|
||||
@@ -21,7 +24,7 @@ module.exports = class TorrentList extends React.Component {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
var torrentElems = state.saved.torrents.map(
|
||||
const torrentElems = state.saved.torrents.map(
|
||||
(torrentSummary) => this.renderTorrent(torrentSummary)
|
||||
)
|
||||
contents.push(...torrentElems)
|
||||
@@ -39,30 +42,27 @@ module.exports = class TorrentList extends React.Component {
|
||||
}
|
||||
|
||||
renderTorrent (torrentSummary) {
|
||||
var state = this.props.state
|
||||
var infoHash = torrentSummary.infoHash
|
||||
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||
const state = this.props.state
|
||||
const infoHash = torrentSummary.infoHash
|
||||
const isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||
|
||||
// Background image: show some nice visuals, like a frame from the movie, if possible
|
||||
var style = {}
|
||||
const style = {}
|
||||
if (torrentSummary.posterFileName) {
|
||||
var gradient = isSelected
|
||||
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
||||
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
|
||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||
style.backgroundImage = gradient + `, url('${posterPath}')`
|
||||
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
||||
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||
style.backgroundImage = `${gradient}, url('${posterPath}')`
|
||||
}
|
||||
|
||||
// Foreground: name of the torrent, basic info like size, play button,
|
||||
// cast buttons if available, and delete
|
||||
var classes = ['torrent']
|
||||
// playStatus turns the play button into a loading spinner or error icon
|
||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
||||
const classes = ['torrent']
|
||||
if (isSelected) classes.push('selected')
|
||||
if (!infoHash) classes.push('disabled')
|
||||
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
||||
return (
|
||||
<div
|
||||
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
|
||||
key={torrentSummary.torrentKey}
|
||||
style={style}
|
||||
className={classes.join(' ')}
|
||||
@@ -71,48 +71,96 @@ module.exports = class TorrentList extends React.Component {
|
||||
{this.renderTorrentMetadata(torrentSummary)}
|
||||
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
|
||||
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
|
||||
<hr />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Show name, download status, % complete
|
||||
renderTorrentMetadata (torrentSummary) {
|
||||
var name = torrentSummary.name || 'Loading torrent...'
|
||||
var elements = [(
|
||||
const name = torrentSummary.name || 'Loading torrent...'
|
||||
const elements = [(
|
||||
<div key='name' className='name ellipsis'>{name}</div>
|
||||
)]
|
||||
|
||||
// If it's downloading/seeding then show progress info
|
||||
var prog = torrentSummary.progress
|
||||
const prog = torrentSummary.progress
|
||||
let progElems
|
||||
if (torrentSummary.error) {
|
||||
elements.push(
|
||||
<div key='progress-info' className='ellipsis'>
|
||||
{getErrorMessage(torrentSummary)}
|
||||
</div>
|
||||
)
|
||||
progElems = [getErrorMessage(torrentSummary)]
|
||||
} else if (torrentSummary.status !== 'paused' && prog) {
|
||||
elements.push(
|
||||
<div key='progress-info' className='ellipsis'>
|
||||
{renderPercentProgress()}
|
||||
{renderTotalProgress()}
|
||||
{renderPeers()}
|
||||
{renderDownloadSpeed()}
|
||||
{renderUploadSpeed()}
|
||||
{renderEta()}
|
||||
progElems = [
|
||||
renderDownloadCheckbox(),
|
||||
renderTorrentStatus(),
|
||||
renderProgressBar(),
|
||||
renderPercentProgress(),
|
||||
renderTotalProgress(),
|
||||
renderPeers(),
|
||||
renderSpeeds(),
|
||||
renderEta()
|
||||
]
|
||||
} else {
|
||||
progElems = [
|
||||
renderDownloadCheckbox(),
|
||||
renderTorrentStatus()
|
||||
]
|
||||
}
|
||||
elements.push(
|
||||
<div key='progress-info' className='ellipsis'>
|
||||
{progElems}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (<div key='metadata' className='metadata'>{elements}</div>)
|
||||
|
||||
function renderDownloadCheckbox () {
|
||||
const infoHash = torrentSummary.infoHash
|
||||
const isActive = ['downloading', 'seeding'].includes(torrentSummary.status)
|
||||
return (
|
||||
<Checkbox
|
||||
key='download-button'
|
||||
className={'control download ' + torrentSummary.status}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: 32
|
||||
}}
|
||||
iconStyle={{
|
||||
width: 20,
|
||||
height: 20
|
||||
}}
|
||||
checked={isActive}
|
||||
onClick={stopPropagation}
|
||||
onCheck={dispatcher('toggleTorrent', infoHash)} />
|
||||
)
|
||||
}
|
||||
|
||||
function renderProgressBar () {
|
||||
const progress = Math.floor(100 * prog.progress)
|
||||
const styles = {
|
||||
wrapper: {
|
||||
display: 'inline-block',
|
||||
marginRight: 8
|
||||
},
|
||||
progress: {
|
||||
height: 8,
|
||||
width: 30
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={styles.wrapper}>
|
||||
<LinearProgress style={styles.progress} mode='determinate' value={progress} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (<div key='metadata' className='metadata'>{elements}</div>)
|
||||
|
||||
function renderPercentProgress () {
|
||||
var progress = Math.floor(100 * prog.progress)
|
||||
const progress = Math.floor(100 * prog.progress)
|
||||
return (<span key='percent-progress'>{progress}%</span>)
|
||||
}
|
||||
|
||||
function renderTotalProgress () {
|
||||
var downloaded = prettyBytes(prog.downloaded)
|
||||
var total = prettyBytes(prog.length || 0)
|
||||
const downloaded = prettyBytes(prog.downloaded)
|
||||
const total = prettyBytes(prog.length || 0)
|
||||
if (downloaded === total) {
|
||||
return (<span key='total-progress'>{downloaded}</span>)
|
||||
} else {
|
||||
@@ -122,111 +170,78 @@ module.exports = class TorrentList extends React.Component {
|
||||
|
||||
function renderPeers () {
|
||||
if (prog.numPeers === 0) return
|
||||
var count = prog.numPeers === 1 ? 'peer' : 'peers'
|
||||
const count = prog.numPeers === 1 ? 'peer' : 'peers'
|
||||
return (<span key='peers'>{prog.numPeers} {count}</span>)
|
||||
}
|
||||
|
||||
function renderDownloadSpeed () {
|
||||
if (prog.downloadSpeed === 0) return
|
||||
return (<span key='download'>↓ {prettyBytes(prog.downloadSpeed)}/s</span>)
|
||||
}
|
||||
|
||||
function renderUploadSpeed () {
|
||||
if (prog.uploadSpeed === 0) return
|
||||
return (<span key='upload'>↑ {prettyBytes(prog.uploadSpeed)}/s</span>)
|
||||
function renderSpeeds () {
|
||||
let str = ''
|
||||
if (prog.downloadSpeed > 0) str += ' ↓ ' + prettyBytes(prog.downloadSpeed) + '/s'
|
||||
if (prog.uploadSpeed > 0) str += ' ↑ ' + prettyBytes(prog.uploadSpeed) + '/s'
|
||||
if (str === '') return
|
||||
return (<span key='download'>{str}</span>)
|
||||
}
|
||||
|
||||
function renderEta () {
|
||||
var downloaded = prog.downloaded
|
||||
var total = prog.length || 0
|
||||
var missing = total - downloaded
|
||||
var downloadSpeed = prog.downloadSpeed
|
||||
const downloaded = prog.downloaded
|
||||
const total = prog.length || 0
|
||||
const missing = total - downloaded
|
||||
const downloadSpeed = prog.downloadSpeed
|
||||
if (downloadSpeed === 0 || missing === 0) return
|
||||
|
||||
var rawEta = missing / downloadSpeed
|
||||
var hours = Math.floor(rawEta / 3600) % 24
|
||||
var minutes = Math.floor(rawEta / 60) % 60
|
||||
var seconds = Math.floor(rawEta % 60)
|
||||
const rawEta = missing / downloadSpeed
|
||||
const hours = Math.floor(rawEta / 3600) % 24
|
||||
const minutes = Math.floor(rawEta / 60) % 60
|
||||
const seconds = Math.floor(rawEta % 60)
|
||||
|
||||
// Only display hours and minutes if they are greater than 0 but always
|
||||
// display minutes if hours is being displayed
|
||||
var hoursStr = hours ? hours + 'h' : ''
|
||||
var minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
||||
var secondsStr = seconds + 's'
|
||||
const hoursStr = hours ? hours + 'h' : ''
|
||||
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
||||
const secondsStr = seconds + 's'
|
||||
|
||||
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
|
||||
return (<span>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
|
||||
}
|
||||
|
||||
function renderTorrentStatus () {
|
||||
let status
|
||||
if (torrentSummary.status === 'paused') {
|
||||
if (!torrentSummary.progress) status = ''
|
||||
else if (torrentSummary.progress.progress === 1) status = 'Not seeding'
|
||||
else status = 'Paused'
|
||||
} else if (torrentSummary.status === 'downloading') {
|
||||
status = 'Downloading'
|
||||
} else if (torrentSummary.status === 'seeding') {
|
||||
status = 'Seeding'
|
||||
} else { // torrentSummary.status is 'new' or something unexpected
|
||||
status = ''
|
||||
}
|
||||
return (<span>{status}</span>)
|
||||
}
|
||||
}
|
||||
|
||||
// Download button toggles between torrenting (DL/seed) and paused
|
||||
// Play button starts streaming the torrent immediately, unpausing if needed
|
||||
renderTorrentButtons (torrentSummary) {
|
||||
var infoHash = torrentSummary.infoHash
|
||||
const infoHash = torrentSummary.infoHash
|
||||
|
||||
var playIcon, playTooltip, playClass
|
||||
if (torrentSummary.playStatus === 'timeout') {
|
||||
playIcon = 'warning'
|
||||
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
||||
} else {
|
||||
playIcon = 'play_arrow'
|
||||
playTooltip = 'Start streaming'
|
||||
}
|
||||
|
||||
var downloadIcon, downloadTooltip
|
||||
if (torrentSummary.status === 'seeding') {
|
||||
downloadIcon = 'file_upload'
|
||||
downloadTooltip = 'Seeding. Click to stop.'
|
||||
} else if (torrentSummary.status === 'downloading') {
|
||||
downloadIcon = 'file_download'
|
||||
downloadTooltip = 'Torrenting. Click to stop.'
|
||||
} else {
|
||||
downloadIcon = 'file_download'
|
||||
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 = this.renderRadialProgressBar(fraction, 'radial-progress-large')
|
||||
playClass = 'resume-position'
|
||||
}
|
||||
|
||||
// Only show the play button for torrents that contain playable media
|
||||
var playButton, downloadButton
|
||||
var noErrors = !torrentSummary.error
|
||||
if (noErrors && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||
// Only show the play/dowload buttons for torrents that contain playable media
|
||||
let playButton
|
||||
if (!torrentSummary.error && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||
playButton = (
|
||||
<i
|
||||
key='play-button'
|
||||
title={playTooltip}
|
||||
className={'button-round icon play ' + playClass}
|
||||
title='Start streaming'
|
||||
className={'icon play'}
|
||||
onClick={dispatcher('playFile', infoHash)}>
|
||||
{playIcon}
|
||||
</i>
|
||||
)
|
||||
}
|
||||
if (noErrors) {
|
||||
downloadButton = (
|
||||
<i
|
||||
key='download-button'
|
||||
className={'button-round icon download ' + torrentSummary.status}
|
||||
title={downloadTooltip}
|
||||
onClick={dispatcher('toggleTorrent', infoHash)}>
|
||||
{downloadIcon}
|
||||
play_circle_outline
|
||||
</i>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key='buttons' className='buttons'>
|
||||
{positionElem}
|
||||
<div className='torrent-controls'>
|
||||
{playButton}
|
||||
{downloadButton}
|
||||
<i
|
||||
key='delete-button'
|
||||
className='icon delete'
|
||||
@@ -240,9 +255,9 @@ module.exports = class TorrentList extends React.Component {
|
||||
|
||||
// Show files, per-file download status and play buttons, and so on
|
||||
renderTorrentDetails (torrentSummary) {
|
||||
var filesElement
|
||||
let filesElement
|
||||
if (torrentSummary.error || !torrentSummary.files) {
|
||||
var message = ''
|
||||
let message = ''
|
||||
if (torrentSummary.error === 'path-missing') {
|
||||
// Special case error: this torrent's download dir or file is missing
|
||||
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
|
||||
@@ -263,14 +278,9 @@ module.exports = class TorrentList extends React.Component {
|
||||
)
|
||||
} else {
|
||||
// We do know the files. List them and show download stats for each one
|
||||
var fileRows = torrentSummary.files
|
||||
const fileRows = torrentSummary.files
|
||||
.filter((file) => !file.path.includes('/.____padding_file/'))
|
||||
.map((file, index) => ({ file, index }))
|
||||
.sort(function (a, b) {
|
||||
if (a.file.name < b.file.name) return -1
|
||||
if (b.file.name < a.file.name) return 1
|
||||
return 0
|
||||
})
|
||||
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
|
||||
|
||||
filesElement = (
|
||||
@@ -295,36 +305,39 @@ module.exports = class TorrentList extends React.Component {
|
||||
renderFileRow (torrentSummary, file, index) {
|
||||
// First, find out how much of the file we've downloaded
|
||||
// Are we even torrenting it?
|
||||
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||
var isDone = false // Are we finished torrenting it?
|
||||
var progress = ''
|
||||
const isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||
let isDone = false // Are we finished torrenting it?
|
||||
let progress = ''
|
||||
if (torrentSummary.progress && torrentSummary.progress.files &&
|
||||
torrentSummary.progress.files[index]) {
|
||||
var fileProg = torrentSummary.progress.files[index]
|
||||
const fileProg = torrentSummary.progress.files[index]
|
||||
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
||||
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
||||
}
|
||||
|
||||
// Second, for media files where we saved our position, show how far we got
|
||||
var positionElem
|
||||
let positionElem
|
||||
if (file.currentTime) {
|
||||
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
|
||||
positionElem = this.renderRadialProgressBar(file.currentTime / file.duration)
|
||||
}
|
||||
|
||||
// Finally, render the file as a table row
|
||||
var isPlayable = TorrentPlayer.isPlayable(file)
|
||||
var infoHash = torrentSummary.infoHash
|
||||
var icon
|
||||
var handleClick
|
||||
const isPlayable = TorrentPlayer.isPlayable(file)
|
||||
const infoHash = torrentSummary.infoHash
|
||||
let icon
|
||||
let handleClick
|
||||
if (isPlayable) {
|
||||
icon = 'play_arrow' /* playable? add option to play */
|
||||
handleClick = dispatcher('playFile', infoHash, index)
|
||||
} else {
|
||||
icon = 'description' /* file icon, opens in OS default app */
|
||||
handleClick = dispatcher('openItem', infoHash, index)
|
||||
handleClick = isDone
|
||||
? dispatcher('openItem', infoHash, index)
|
||||
: (e) => e.stopPropagation() // noop if file is not ready
|
||||
}
|
||||
var rowClass = ''
|
||||
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed
|
||||
let rowClass = ''
|
||||
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||
return (
|
||||
@@ -344,16 +357,16 @@ module.exports = class TorrentList extends React.Component {
|
||||
</td>
|
||||
<td className='col-select'
|
||||
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
|
||||
<i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
renderRadialProgressBar (fraction, cssClass) {
|
||||
var rotation = 360 * fraction
|
||||
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
||||
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
||||
const rotation = 360 * fraction
|
||||
const transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
||||
const transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
||||
|
||||
return (
|
||||
<div key='radial-progress' className={'radial-progress ' + cssClass}>
|
||||
@@ -372,8 +385,12 @@ module.exports = class TorrentList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
function stopPropagation (e) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
function getErrorMessage (torrentSummary) {
|
||||
var err = torrentSummary.error
|
||||
const err = torrentSummary.error
|
||||
if (err === 'path-missing') {
|
||||
return (
|
||||
<span>
|
||||
@@ -1,97 +0,0 @@
|
||||
const React = require('react')
|
||||
|
||||
const Header = require('./header')
|
||||
|
||||
const Views = {
|
||||
'home': require('./torrent-list'),
|
||||
'player': require('./player'),
|
||||
'create-torrent': require('./create-torrent'),
|
||||
'preferences': require('./preferences')
|
||||
}
|
||||
|
||||
const Modals = {
|
||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||
'remove-torrent-modal': require('./remove-torrent-modal'),
|
||||
'update-available-modal': require('./update-available-modal'),
|
||||
'unsupported-media-modal': require('./unsupported-media-modal')
|
||||
}
|
||||
|
||||
module.exports = class App extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = props.state
|
||||
}
|
||||
|
||||
render () {
|
||||
var state = this.state
|
||||
|
||||
// Hide player controls while playing video, if the mouse stays still for a while
|
||||
// Never hide the controls when:
|
||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||
// * The video is paused
|
||||
// * The video is playing remotely on Chromecast or Airplay
|
||||
var hideControls = state.location.url() === 'player' &&
|
||||
state.playing.mouseStationarySince !== 0 &&
|
||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||
!state.playing.isPaused &&
|
||||
state.playing.location === 'local' &&
|
||||
state.playing.playbackRate === 1
|
||||
|
||||
var cls = [
|
||||
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
||||
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
||||
]
|
||||
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
||||
if (state.window.isFocused) cls.push('is-focused')
|
||||
if (hideControls) cls.push('hide-video-controls')
|
||||
|
||||
var vdom = (
|
||||
<div className={'app ' + cls.join(' ')}>
|
||||
<Header state={state} />
|
||||
{this.getErrorPopover()}
|
||||
<div key='content' className='content'>{this.getView()}</div>
|
||||
{this.getModal()}
|
||||
</div>
|
||||
)
|
||||
|
||||
return vdom
|
||||
}
|
||||
|
||||
getErrorPopover () {
|
||||
var now = new Date().getTime()
|
||||
var recentErrors = this.state.errors.filter((x) => now - x.time < 5000)
|
||||
var hasErrors = recentErrors.length > 0
|
||||
|
||||
var errorElems = recentErrors.map(function (error, i) {
|
||||
return (<div key={i} className='error'>{error.message}</div>)
|
||||
})
|
||||
return (
|
||||
<div key='errors'
|
||||
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
|
||||
<div key='title' className='title'>Error</div>
|
||||
{errorElems}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getModal () {
|
||||
var state = this.state
|
||||
if (!state.modal) return
|
||||
var ModalContents = Modals[state.modal.id]
|
||||
return (
|
||||
<div key='modal' className='modal'>
|
||||
<div key='modal-background' className='modal-background' />
|
||||
<div key='modal-content' className='modal-content'>
|
||||
<ModalContents state={state} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getView () {
|
||||
var state = this.state
|
||||
var View = Views[state.location.url()]
|
||||
return (<View state={state} />)
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
const React = require('react')
|
||||
const createTorrent = require('create-torrent')
|
||||
const path = require('path')
|
||||
const prettyBytes = require('prettier-bytes')
|
||||
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
const CreateTorrentErrorPage = require('./create-torrent-error-page')
|
||||
|
||||
module.exports = class CreateTorrentPage extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
var info = state.location.current()
|
||||
|
||||
// Preprocess: exclude .DS_Store and other dotfiles
|
||||
var files = info.files
|
||||
.filter((f) => !f.name.startsWith('.'))
|
||||
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
||||
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
|
||||
|
||||
// First, extract the base folder that the files are all in
|
||||
var pathPrefix = info.folderPath
|
||||
if (!pathPrefix) {
|
||||
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||
pathPrefix = path.dirname(pathPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check: show the number of files and total size
|
||||
var numFiles = files.length
|
||||
var totalBytes = files
|
||||
.map((f) => f.size)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
|
||||
|
||||
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
||||
// as the default name. Show all files relative to the base folder.
|
||||
var defaultName, basePath
|
||||
if (files.length === 1) {
|
||||
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
|
||||
defaultName = files[0].name
|
||||
basePath = pathPrefix
|
||||
} else {
|
||||
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a'
|
||||
defaultName = path.basename(pathPrefix)
|
||||
basePath = path.dirname(pathPrefix)
|
||||
}
|
||||
var maxFileElems = 100
|
||||
var fileElems = files.slice(0, maxFileElems).map(function (file, i) {
|
||||
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
|
||||
return (<div key={i}>{relativePath}</div>)
|
||||
})
|
||||
if (files.length > maxFileElems) {
|
||||
fileElems.push(<div key='more'>+ {maxFileElems - files.length} more</div>)
|
||||
}
|
||||
var trackers = createTorrent.announceList.join('\n')
|
||||
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
|
||||
|
||||
return (
|
||||
<div className='create-torrent'>
|
||||
<h2>Create torrent {defaultName}</h2>
|
||||
<div key='info' className='torrent-info'>
|
||||
{torrentInfo}
|
||||
</div>
|
||||
<div key='path-prefix' className='torrent-attribute'>
|
||||
<label>Path:</label>
|
||||
<div className='torrent-attribute'>{pathPrefix}</div>
|
||||
</div>
|
||||
<div key='toggle' className={'expand-collapse ' + collapsedClass}
|
||||
onClick={dispatcher('toggleCreateTorrentAdvanced')}>
|
||||
{info.showAdvanced ? 'Basic' : 'Advanced'}
|
||||
</div>
|
||||
<div key='advanced' className={'create-torrent-advanced ' + collapsedClass}>
|
||||
<div key='comment' className='torrent-attribute'>
|
||||
<label>Comment:</label>
|
||||
<textarea className='torrent-attribute torrent-comment' />
|
||||
</div>
|
||||
<div key='trackers' className='torrent-attribute'>
|
||||
<label>Trackers:</label>
|
||||
<textarea className='torrent-attribute torrent-trackers' defaultValue={trackers} />
|
||||
</div>
|
||||
<div key='private' className='torrent-attribute'>
|
||||
<label>Private:</label>
|
||||
<input type='checkbox' className='torrent-is-private' value='torrent-is-private' />
|
||||
</div>
|
||||
<div key='files' className='torrent-attribute'>
|
||||
<label>Files:</label>
|
||||
<div>{fileElems}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div key='buttons' className='float-right'>
|
||||
<button key='cancel' className='button-flat light' onClick={dispatcher('cancel')}>Cancel</button>
|
||||
<button key='create' className='button-raised' onClick={handleOK}>Create Torrent</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
function handleOK () {
|
||||
// TODO: dcposch use React refs instead
|
||||
var announceList = document.querySelector('.torrent-trackers').value
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== '')
|
||||
var isPrivate = document.querySelector('.torrent-is-private').checked
|
||||
var comment = document.querySelector('.torrent-comment').value.trim()
|
||||
var options = {
|
||||
// We can't let the user choose their own name if we want WebTorrent
|
||||
// to use the files in place rather than creating a new folder.
|
||||
// If we ever want to add support for that:
|
||||
// name: document.querySelector('.torrent-name').value
|
||||
name: defaultName,
|
||||
path: basePath,
|
||||
files: files,
|
||||
announce: announceList,
|
||||
private: isPrivate,
|
||||
comment: comment
|
||||
}
|
||||
dispatch('createTorrent', options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finds the longest common prefix
|
||||
function findCommonPrefix (a, b) {
|
||||
for (var i = 0; i < a.length && i < b.length; i++) {
|
||||
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
||||
}
|
||||
if (i === a.length) return a
|
||||
if (i === b.length) return b
|
||||
return a.substring(0, i)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
const React = require('react')
|
||||
|
||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class OpenTorrentAddressModal extends React.Component {
|
||||
render () {
|
||||
// TODO: dcposch remove janky inline <script>
|
||||
return (
|
||||
<div className='open-torrent-address-modal'>
|
||||
<p><label>Enter torrent address or magnet link</label></p>
|
||||
<p>
|
||||
<input id='add-torrent-url' type='text' onKeyPress={handleKeyPress} />
|
||||
</p>
|
||||
<p className='float-right'>
|
||||
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
|
||||
<button className='button button-raised' onClick={handleOK}>OK</button>
|
||||
</p>
|
||||
<script>document.querySelector('#add-torrent-url').focus()</script>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyPress (e) {
|
||||
if (e.which === 13) handleOK() /* hit Enter to submit */
|
||||
}
|
||||
|
||||
function handleOK () {
|
||||
dispatch('exitModal')
|
||||
// TODO: dcposch use React refs instead
|
||||
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
const React = require('react')
|
||||
const remote = require('electron').remote
|
||||
const dialog = remote.dialog
|
||||
const path = require('path')
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class Preferences extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
return (
|
||||
<div className='preferences'>
|
||||
{renderGeneralSection(state)}
|
||||
{renderPlaybackSection(state)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function renderGeneralSection (state) {
|
||||
return renderSection({
|
||||
key: 'general',
|
||||
title: 'General',
|
||||
description: '',
|
||||
icon: 'settings'
|
||||
}, [
|
||||
renderDownloadPathSelector(state),
|
||||
renderFileHandlers(state)
|
||||
])
|
||||
}
|
||||
|
||||
function renderPlaybackSection (state) {
|
||||
return renderSection({
|
||||
title: 'Playback',
|
||||
description: '',
|
||||
icon: 'settings'
|
||||
}, [
|
||||
renderOpenExternalPlayerSelector(state),
|
||||
renderExternalPlayerSelector(state)
|
||||
])
|
||||
}
|
||||
|
||||
function renderDownloadPathSelector (state) {
|
||||
return renderFileSelector({
|
||||
key: 'download-path',
|
||||
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) {
|
||||
dispatch('updatePreferences', 'downloadPath', filePath)
|
||||
})
|
||||
}
|
||||
|
||||
function renderFileHandlers (state) {
|
||||
var definition = {
|
||||
key: 'file-handlers',
|
||||
label: 'Handle Torrent Files'
|
||||
}
|
||||
var buttonText = state.unsaved.prefs.isFileHandler
|
||||
? 'Remove default app for torrent files'
|
||||
: 'Make WebTorrent the default app for torrent files'
|
||||
var controls = [(
|
||||
<button key='toggle-handlers'
|
||||
className='btn'
|
||||
onClick={toggleFileHandlers}>
|
||||
{buttonText}
|
||||
</button>
|
||||
)]
|
||||
return renderControlGroup(definition, controls)
|
||||
|
||||
function toggleFileHandlers () {
|
||||
var isFileHandler = state.unsaved.prefs.isFileHandler
|
||||
dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
|
||||
}
|
||||
}
|
||||
|
||||
function renderExternalPlayerSelector (state) {
|
||||
return renderFileSelector({
|
||||
label: 'External Media Player',
|
||||
description: 'Progam that will be used to play media externally',
|
||||
property: 'externalPlayerPath',
|
||||
options: {
|
||||
title: 'Select media player executable',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
},
|
||||
state.unsaved.prefs.externalPlayerPath || '<VLC>',
|
||||
function (filePath) {
|
||||
if (path.extname(filePath) === '.app') {
|
||||
// Get executable in packaged mac app
|
||||
var name = path.basename(filePath, '.app')
|
||||
filePath += '/Contents/MacOS/' + name
|
||||
}
|
||||
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
||||
})
|
||||
}
|
||||
|
||||
function renderOpenExternalPlayerSelector (state) {
|
||||
return renderCheckbox({
|
||||
key: 'open-external-player',
|
||||
label: 'Play in External Player',
|
||||
description: 'Media will play in external player',
|
||||
property: 'openExternalPlayer',
|
||||
value: state.saved.prefs.openExternalPlayer
|
||||
},
|
||||
state.unsaved.prefs.openExternalPlayer,
|
||||
function (value) {
|
||||
dispatch('updatePreferences', 'openExternalPlayer', value)
|
||||
})
|
||||
}
|
||||
|
||||
// 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 : (
|
||||
<div key='help' className='help text'>
|
||||
<i className='icon'>help_outline</i>{definition.description}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<section key={definition.key} className='section preferences-panel'>
|
||||
<div className='section-container'>
|
||||
<div key='heading' className='section-heading'>
|
||||
<i className='icon'>{definition.icon}</i>{definition.title}
|
||||
</div>
|
||||
{helpElem}
|
||||
<div key='body' className='section-body'>
|
||||
{controls}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function renderCheckbox (definition, value, callback) {
|
||||
var iconClass = 'icon clickable'
|
||||
if (value) iconClass += ' enabled'
|
||||
|
||||
return (
|
||||
<div key='{definition.key}' className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='preference-title'>{definition.label}</div>
|
||||
</label>
|
||||
<div className='controls'>
|
||||
<label className='clickable' onClick={handleClick}>
|
||||
<i
|
||||
className={iconClass}
|
||||
id='{definition.property}'
|
||||
>
|
||||
check_circle
|
||||
</i>
|
||||
<span className='checkbox-label'>{definition.description}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
function handleClick () {
|
||||
callback(!value)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
var controls = [(
|
||||
<input
|
||||
type='text'
|
||||
className='file-picker-text'
|
||||
key={definition.property}
|
||||
id={definition.property}
|
||||
disabled='disabled'
|
||||
value={value} />
|
||||
), (
|
||||
<button
|
||||
key={definition.property + '-btn'}
|
||||
className='btn'
|
||||
onClick={handleClick}>
|
||||
<i className='icon'>folder_open</i>
|
||||
</button>
|
||||
)]
|
||||
return renderControlGroup(definition, controls)
|
||||
|
||||
function handleClick () {
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
||||
if (!Array.isArray(filenames)) return
|
||||
callback(filenames[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function renderControlGroup (definition, controls) {
|
||||
return (
|
||||
<div key={definition.key} className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='preference-title'>{definition.label}</div>
|
||||
<div className='preference-description'>{definition.description}</div>
|
||||
</label>
|
||||
<div className='controls'>
|
||||
{controls}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
const React = require('react')
|
||||
const electron = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
const {dispatcher} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class UnsupportedMediaModal extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
var err = state.modal.error
|
||||
var message = (err && err.getMessage)
|
||||
? err.getMessage()
|
||||
: err
|
||||
var playerPath = state.saved.prefs.externalPlayerPath
|
||||
var playerName = playerPath
|
||||
? path.basename(playerPath).split('.')[0]
|
||||
: 'VLC'
|
||||
var actionButton = state.modal.externalPlayerInstalled
|
||||
? (<button className='button-raised' onClick={dispatcher('openExternalPlayer')}>Play in {playerName}</button>)
|
||||
: (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>)
|
||||
var playerMessage = state.modal.externalPlayerNotFound
|
||||
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
||||
: ''
|
||||
return (
|
||||
<div>
|
||||
<p><strong>Sorry, we can't play that file.</strong></p>
|
||||
<p>{message}</p>
|
||||
<p className='float-right'>
|
||||
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
|
||||
{actionButton}
|
||||
</p>
|
||||
<p className='error-text'>{playerMessage}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onInstall () {
|
||||
electron.shell.openExternal('http://www.videolan.org/vlc/')
|
||||
|
||||
// TODO: dcposch send a dispatch rather than modifying state directly
|
||||
var state = this.props.state
|
||||
state.modal.externalPlayerInstalled = true // Assume they'll install it successfully
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,28 @@
|
||||
// process from the main window.
|
||||
console.time('init')
|
||||
|
||||
var deepEqual = require('deep-equal')
|
||||
var defaultAnnounceList = require('create-torrent').announceList
|
||||
var electron = require('electron')
|
||||
var fs = require('fs-extra')
|
||||
var hat = require('hat')
|
||||
var musicmetadata = require('musicmetadata')
|
||||
var networkAddress = require('network-address')
|
||||
var path = require('path')
|
||||
var WebTorrent = require('webtorrent')
|
||||
var zeroFill = require('zero-fill')
|
||||
const crypto = require('crypto')
|
||||
const deepEqual = require('deep-equal')
|
||||
const defaultAnnounceList = require('create-torrent').announceList
|
||||
const electron = require('electron')
|
||||
const fs = require('fs')
|
||||
const mkdirp = require('mkdirp')
|
||||
const musicmetadata = require('musicmetadata')
|
||||
const networkAddress = require('network-address')
|
||||
const path = require('path')
|
||||
const WebTorrent = require('webtorrent')
|
||||
const zeroFill = require('zero-fill')
|
||||
|
||||
var crashReporter = require('../crash-reporter')
|
||||
var config = require('../config')
|
||||
var torrentPoster = require('./lib/torrent-poster')
|
||||
const crashReporter = require('../crash-reporter')
|
||||
const config = require('../config')
|
||||
const {TorrentKeyNotFoundError} = require('./lib/errors')
|
||||
const torrentPoster = require('./lib/torrent-poster')
|
||||
|
||||
// Report when the process crashes
|
||||
crashReporter.init()
|
||||
|
||||
// Send & receive messages from the main window
|
||||
var ipc = electron.ipcRenderer
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
// Force use of webtorrent trackers on all torrents
|
||||
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
||||
@@ -31,7 +33,7 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
||||
/**
|
||||
* WebTorrent version.
|
||||
*/
|
||||
var VERSION = require('../../package.json').version
|
||||
const VERSION = require('../../package.json').version
|
||||
|
||||
/**
|
||||
* Version number in Azureus-style. Generated from major and minor semver version.
|
||||
@@ -39,7 +41,7 @@ var VERSION = require('../../package.json').version
|
||||
* '0.16.1' -> '0016'
|
||||
* '1.2.5' -> '0102'
|
||||
*/
|
||||
var VERSION_STR = VERSION.match(/([0-9]+)/g)
|
||||
const VERSION_STR = VERSION.match(/([0-9]+)/g)
|
||||
.slice(0, 2)
|
||||
.map((v) => zeroFill(2, v))
|
||||
.join('')
|
||||
@@ -51,25 +53,27 @@ var VERSION_STR = VERSION.match(/([0-9]+)/g)
|
||||
* For example:
|
||||
* '-WW0102-'...
|
||||
*/
|
||||
var VERSION_PREFIX = '-WD' + VERSION_STR + '-'
|
||||
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'
|
||||
|
||||
/**
|
||||
* Generate an ephemeral peer ID each time.
|
||||
*/
|
||||
const PEER_ID = Buffer.from(VERSION_PREFIX + crypto.randomBytes(9).toString('base64'))
|
||||
|
||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||
// client, as explained here: https://webtorrent.io/faq
|
||||
var client = window.client = new WebTorrent({
|
||||
peerId: Buffer.from(VERSION_PREFIX + hat(48))
|
||||
})
|
||||
let client = window.client = new WebTorrent({ peerId: PEER_ID })
|
||||
|
||||
// WebTorrent-to-HTTP streaming sever
|
||||
var server = null
|
||||
let server = null
|
||||
|
||||
// Used for diffing, so we only send progress updates when necessary
|
||||
var prevProgress = null
|
||||
let prevProgress = null
|
||||
|
||||
init()
|
||||
|
||||
function init () {
|
||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||
listenToClientEvents()
|
||||
|
||||
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
||||
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
||||
@@ -83,8 +87,8 @@ function init () {
|
||||
generateTorrentPoster(torrentKey))
|
||||
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
||||
getAudioMetadata(infoHash, index))
|
||||
ipc.on('wt-start-server', (e, infoHash, index) =>
|
||||
startServer(infoHash, index))
|
||||
ipc.on('wt-start-server', (e, infoHash) =>
|
||||
startServer(infoHash))
|
||||
ipc.on('wt-stop-server', (e) =>
|
||||
stopServer())
|
||||
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
||||
@@ -97,14 +101,20 @@ function init () {
|
||||
true)
|
||||
|
||||
setInterval(updateTorrentProgress, 1000)
|
||||
console.timeEnd('init')
|
||||
}
|
||||
|
||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
||||
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||
function listenToClientEvents () {
|
||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||
}
|
||||
|
||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
|
||||
// Returns a WebTorrent object. See https://git.io/vik9M
|
||||
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
||||
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
||||
|
||||
var torrent = client.add(torrentID, {
|
||||
const torrent = client.add(torrentID, {
|
||||
path: path,
|
||||
fileModtimes: fileModtimes
|
||||
})
|
||||
@@ -120,15 +130,15 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
|
||||
}
|
||||
|
||||
function stopTorrenting (infoHash) {
|
||||
var torrent = client.get(infoHash)
|
||||
const torrent = client.get(infoHash)
|
||||
if (torrent) torrent.destroy()
|
||||
}
|
||||
|
||||
// Create a new torrent, start seeding
|
||||
function createTorrent (torrentKey, options) {
|
||||
console.log('creating torrent', torrentKey, options)
|
||||
var paths = options.files.map((f) => f.path)
|
||||
var torrent = client.seed(paths, options)
|
||||
const paths = options.files.map((f) => f.path)
|
||||
const torrent = client.seed(paths, options)
|
||||
torrent.key = torrentKey
|
||||
addTorrentEvents(torrent)
|
||||
ipc.send('wt-new-torrent')
|
||||
@@ -146,22 +156,22 @@ function addTorrentEvents (torrent) {
|
||||
torrent.on('done', torrentDone)
|
||||
|
||||
function torrentMetadata () {
|
||||
var info = getTorrentInfo(torrent)
|
||||
const info = getTorrentInfo(torrent)
|
||||
ipc.send('wt-metadata', torrent.key, info)
|
||||
|
||||
updateTorrentProgress()
|
||||
}
|
||||
|
||||
function torrentReady () {
|
||||
var info = getTorrentInfo(torrent)
|
||||
const info = getTorrentInfo(torrent)
|
||||
ipc.send('wt-ready', torrent.key, info)
|
||||
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
|
||||
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
|
||||
|
||||
updateTorrentProgress()
|
||||
}
|
||||
|
||||
function torrentDone () {
|
||||
var info = getTorrentInfo(torrent)
|
||||
const info = getTorrentInfo(torrent)
|
||||
ipc.send('wt-done', torrent.key, info)
|
||||
|
||||
updateTorrentProgress()
|
||||
@@ -194,19 +204,22 @@ function getTorrentFileInfo (file) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every time we resolve a magnet URI, save the torrent file so that we never
|
||||
// have to download it again. Never ask the DHT the same question twice.
|
||||
// Every time we resolve a magnet URI, save the torrent file so that we can use
|
||||
// it on next startup. Starting with the full torrent metadata will be faster
|
||||
// than re-fetching it from peers using ut_metadata.
|
||||
function saveTorrentFile (torrentKey) {
|
||||
var torrent = getTorrent(torrentKey)
|
||||
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
|
||||
var fileName = torrent.infoHash + '.torrent'
|
||||
if (exists) {
|
||||
const torrent = getTorrent(torrentKey)
|
||||
const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
|
||||
|
||||
fs.access(torrentPath, fs.constants.R_OK, function (err) {
|
||||
const fileName = torrent.infoHash + '.torrent'
|
||||
if (!err) {
|
||||
// We've already saved the file
|
||||
return ipc.send('wt-file-saved', torrentKey, fileName)
|
||||
}
|
||||
|
||||
// Otherwise, save the .torrent file, under the app config folder
|
||||
fs.mkdir(config.TORRENT_PATH, function (_) {
|
||||
mkdirp(config.TORRENT_PATH, function (_) {
|
||||
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||
console.log('saved torrent file %s', torrentPath)
|
||||
@@ -216,26 +229,17 @@ function saveTorrentFile (torrentKey) {
|
||||
})
|
||||
}
|
||||
|
||||
// Checks whether we've already resolved a given infohash to a torrent file
|
||||
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
||||
function checkIfTorrentFileExists (infoHash, cb) {
|
||||
var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||
fs.exists(torrentPath, function (exists) {
|
||||
cb(torrentPath, exists)
|
||||
})
|
||||
}
|
||||
|
||||
// Save a JPG that represents a torrent.
|
||||
// Auto chooses either a frame from a video file, an image, etc
|
||||
function generateTorrentPoster (torrentKey) {
|
||||
var torrent = getTorrent(torrentKey)
|
||||
const torrent = getTorrent(torrentKey)
|
||||
torrentPoster(torrent, function (err, buf, extension) {
|
||||
if (err) return console.log('error generating poster: %o', err)
|
||||
// save it for next time
|
||||
fs.mkdirp(config.POSTER_PATH, function (err) {
|
||||
mkdirp(config.POSTER_PATH, function (err) {
|
||||
if (err) return console.log('error creating poster dir: %o', err)
|
||||
var posterFileName = torrent.infoHash + extension
|
||||
var posterFilePath = path.join(config.POSTER_PATH, posterFileName)
|
||||
const posterFileName = torrent.infoHash + extension
|
||||
const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
|
||||
fs.writeFile(posterFilePath, buf, function (err) {
|
||||
if (err) return console.log('error saving poster: %o', err)
|
||||
// show the poster
|
||||
@@ -246,7 +250,7 @@ function generateTorrentPoster (torrentKey) {
|
||||
}
|
||||
|
||||
function updateTorrentProgress () {
|
||||
var progress = getTorrentProgress()
|
||||
const progress = getTorrentProgress()
|
||||
// TODO: diff torrent-by-torrent, not once for the whole update
|
||||
if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
|
||||
return /* don't send heavy object if it hasn't changed */
|
||||
@@ -257,19 +261,19 @@ function updateTorrentProgress () {
|
||||
|
||||
function getTorrentProgress () {
|
||||
// First, track overall progress
|
||||
var progress = client.progress
|
||||
var hasActiveTorrents = client.torrents.some(function (torrent) {
|
||||
const progress = client.progress
|
||||
const hasActiveTorrents = client.torrents.some(function (torrent) {
|
||||
return torrent.progress !== 1
|
||||
})
|
||||
|
||||
// Track progress for every file in each torrent
|
||||
// TODO: ideally this would be tracked by WebTorrent, which could do it
|
||||
// more efficiently than looping over torrent.bitfield
|
||||
var torrentProg = client.torrents.map(function (torrent) {
|
||||
var fileProg = torrent.files && torrent.files.map(function (file, index) {
|
||||
var numPieces = file._endPiece - file._startPiece + 1
|
||||
var numPiecesPresent = 0
|
||||
for (var piece = file._startPiece; piece <= file._endPiece; piece++) {
|
||||
const torrentProg = client.torrents.map(function (torrent) {
|
||||
const fileProg = torrent.files && torrent.files.map(function (file, index) {
|
||||
const numPieces = file._endPiece - file._startPiece + 1
|
||||
let numPiecesPresent = 0
|
||||
for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
|
||||
if (torrent.bitfield.get(piece)) numPiecesPresent++
|
||||
}
|
||||
return {
|
||||
@@ -300,28 +304,28 @@ function getTorrentProgress () {
|
||||
}
|
||||
}
|
||||
|
||||
function startServer (infoHash, index) {
|
||||
var torrent = client.get(infoHash)
|
||||
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
|
||||
else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
|
||||
function startServer (infoHash) {
|
||||
const torrent = client.get(infoHash)
|
||||
if (torrent.ready) startServerFromReadyTorrent(torrent)
|
||||
else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
|
||||
}
|
||||
|
||||
function startServerFromReadyTorrent (torrent, index, cb) {
|
||||
function startServerFromReadyTorrent (torrent, cb) {
|
||||
if (server) return
|
||||
|
||||
// start the streaming torrent-to-http server
|
||||
server = torrent.createServer()
|
||||
server.listen(0, function () {
|
||||
var port = server.address().port
|
||||
var urlSuffix = ':' + port + '/' + index
|
||||
var info = {
|
||||
const port = server.address().port
|
||||
const urlSuffix = ':' + port
|
||||
const info = {
|
||||
torrentKey: torrent.key,
|
||||
localURL: 'http://localhost' + urlSuffix,
|
||||
networkURL: 'http://' + networkAddress() + urlSuffix
|
||||
}
|
||||
|
||||
ipc.send('wt-server-running', info)
|
||||
ipc.send('wt-server-' + torrent.infoHash, info) // TODO: hack
|
||||
ipc.send('wt-server-' + torrent.infoHash, info)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -332,23 +336,28 @@ function stopServer () {
|
||||
}
|
||||
|
||||
function getAudioMetadata (infoHash, index) {
|
||||
var torrent = client.get(infoHash)
|
||||
var file = torrent.files[index]
|
||||
const torrent = client.get(infoHash)
|
||||
const file = torrent.files[index]
|
||||
musicmetadata(file.createReadStream(), function (err, info) {
|
||||
if (err) return
|
||||
console.log('got audio metadata for %s: %o', file.name, info)
|
||||
ipc.send('wt-audio-metadata', infoHash, index, info)
|
||||
if (err) return console.log('error getting audio metadata for ' + infoHash + ':' + index, err)
|
||||
const { artist, album, albumartist, title, year, track, disk, genre } = info
|
||||
const importantInfo = { artist, album, albumartist, title, year, track, disk, genre }
|
||||
console.log('got audio metadata for %s: %o', file.name, importantInfo)
|
||||
ipc.send('wt-audio-metadata', infoHash, index, importantInfo)
|
||||
})
|
||||
}
|
||||
|
||||
function selectFiles (torrentOrInfoHash, selections) {
|
||||
// Get the torrent object
|
||||
var torrent
|
||||
let torrent
|
||||
if (typeof torrentOrInfoHash === 'string') {
|
||||
torrent = client.get(torrentOrInfoHash)
|
||||
} else {
|
||||
torrent = torrentOrInfoHash
|
||||
}
|
||||
if (!torrent) {
|
||||
throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
|
||||
}
|
||||
|
||||
// Selections not specified?
|
||||
// Load all files. We still need to replace the default whole-torrent
|
||||
@@ -368,8 +377,8 @@ function selectFiles (torrentOrInfoHash, selections) {
|
||||
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]
|
||||
for (let i = 0; i < selections.length; i++) {
|
||||
const file = torrent.files[i]
|
||||
if (selections[i]) {
|
||||
file.select()
|
||||
} else {
|
||||
@@ -382,11 +391,25 @@ function selectFiles (torrentOrInfoHash, selections) {
|
||||
// Gets a WebTorrent handle by torrentKey
|
||||
// Throws an Error if we're not currently torrenting anything w/ that key
|
||||
function getTorrent (torrentKey) {
|
||||
var ret = client.torrents.find((x) => x.key === torrentKey)
|
||||
if (!ret) throw new Error('missing torrent key ' + torrentKey)
|
||||
const ret = client.torrents.find((x) => x.key === torrentKey)
|
||||
if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
|
||||
return ret
|
||||
}
|
||||
|
||||
function onError (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
// TODO: remove this once the following bugs are fixed:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
|
||||
// https://github.com/electron/electron/issues/7212
|
||||
window.testOfflineMode = function () {
|
||||
console.log('Test, going OFFLINE')
|
||||
client = window.client = new WebTorrent({
|
||||
peerId: PEER_ID,
|
||||
tracker: false,
|
||||
dht: false,
|
||||
webSeeds: false
|
||||
})
|
||||
listenToClientEvents()
|
||||
}
|
||||
|
||||
0
static/MaterialIcons-Regular.woff2
Normal file → Executable file
BIN
static/NextTrackThumbnailBarButton.png
Normal file
|
After Width: | Height: | Size: 225 B |
BIN
static/PreviousTrackThumbnailBarButton.png
Normal file
|
After Width: | Height: | Size: 233 B |
@@ -32,6 +32,7 @@
|
||||
<p>
|
||||
Version <script>document.write(require('../package.json').version)</script>
|
||||
(<script>document.write(require('webtorrent/package.json').version)</script>)
|
||||
(<script>document.write(process.arch === 'x64' ? '64-bit' : '32-bit')</script>)
|
||||
</p>
|
||||
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
||||
</body>
|
||||
|
||||
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 743 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
482
static/main.css
@@ -19,13 +19,17 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #FFF;
|
||||
.app {
|
||||
color: #FAFAFA; /* grey50 */
|
||||
font-family: BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.app.is-win32 {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
#body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -71,12 +75,12 @@ table {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
background: rgb(40, 40, 40);
|
||||
background: rgb(30, 30, 30);
|
||||
animation: fadein 0.5s;
|
||||
}
|
||||
|
||||
.app:not(.is-focused) {
|
||||
background: rgb(50, 50, 50);
|
||||
background: rgb(40, 40, 40);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -87,9 +91,7 @@ table {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(MaterialIcons-Regular.woff2) format('woff2');
|
||||
src: url('MaterialIcons-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
.icon {
|
||||
@@ -122,10 +124,6 @@ table {
|
||||
* UTILITY CLASSES
|
||||
*/
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -140,33 +138,13 @@ table {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.expand-collapse.expanded::before {
|
||||
content: '▲'
|
||||
}
|
||||
|
||||
.expand-collapse.collapsed::before {
|
||||
content: '▼'
|
||||
}
|
||||
|
||||
.expand-collapse::before {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.expand-collapse.collapsed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* HEADER
|
||||
*/
|
||||
|
||||
.header {
|
||||
background: rgb(40, 40, 40);
|
||||
border-bottom: 1px solid rgb(20, 20, 20);
|
||||
border-bottom: 1px solid rgb(30, 30, 30);
|
||||
height: 38px; /* vertically center OS menu buttons (OS X) */
|
||||
padding-top: 7px;
|
||||
overflow: hidden;
|
||||
@@ -243,7 +221,7 @@ table {
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
flex: 1 1 auto;
|
||||
margin-top: 37px;
|
||||
margin-top: 38px;
|
||||
}
|
||||
|
||||
.app.view-player .content {
|
||||
@@ -287,10 +265,6 @@ table {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.open-torrent-address-modal input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-torrent {
|
||||
padding: 10px 25px;
|
||||
overflow: hidden;
|
||||
@@ -298,6 +272,7 @@ table {
|
||||
|
||||
.create-torrent .torrent-attribute {
|
||||
white-space: nowrap;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.create-torrent .torrent-attribute>* {
|
||||
@@ -305,13 +280,12 @@ table {
|
||||
}
|
||||
|
||||
.create-torrent .torrent-attribute label {
|
||||
width: 60px;
|
||||
margin-right: 10px;
|
||||
width: 100px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.create-torrent .torrent-attribute>div {
|
||||
width: calc(100% - 90px);
|
||||
width: calc(100% - 100px);
|
||||
}
|
||||
|
||||
.create-torrent .torrent-attribute div {
|
||||
@@ -320,29 +294,8 @@ table {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.create-torrent .torrent-attribute textarea {
|
||||
width: calc(100% - 80px);
|
||||
height: 80px;
|
||||
color: #eee;
|
||||
background-color: transparent;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
border-radius: 2px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.create-torrent textarea.torrent-trackers {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.create-torrent input.torrent-is-private {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* BUTTONS
|
||||
* See https://www.google.com/design/spec/components/buttons.html
|
||||
*/
|
||||
|
||||
a,
|
||||
@@ -356,71 +309,13 @@ i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
|
||||
-webkit-filter: brightness(1.3);
|
||||
}
|
||||
|
||||
.button-round { /* Circular icon buttons, used on <i> tags */
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
font-size: 22px;
|
||||
transition: all 0.1s ease-out;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button { /* Rectangular text buttons */
|
||||
background: transparent;
|
||||
margin-left: 10px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #aaa;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button.button-flat {
|
||||
color: #222;
|
||||
padding: 7px 18px;
|
||||
}
|
||||
|
||||
button.button-flat.light {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
button.button-flat:hover,
|
||||
button.button-flat:focus { /* Material design: focused */
|
||||
background-color: rgba(153, 153, 153, 0.2);
|
||||
}
|
||||
|
||||
button.button-flat:active { /* Material design: pressed */
|
||||
background-color: rgba(153, 153, 153, 0.4);
|
||||
}
|
||||
|
||||
button.button-raised {
|
||||
background-color: #2196f3;
|
||||
color: #eee;
|
||||
padding: 7px 18px;
|
||||
}
|
||||
|
||||
button.button-raised:hover,
|
||||
button.button-raised:focus {
|
||||
background-color: #38a0f5;
|
||||
}
|
||||
|
||||
button.button-raised:active {
|
||||
background-color: #51abf6;
|
||||
}
|
||||
|
||||
/*
|
||||
* OTHER FORM ELEMENT DEFAULTS
|
||||
* INPUTS
|
||||
*/
|
||||
|
||||
input[type='text'] {
|
||||
background: transparent;
|
||||
width: 300px;
|
||||
padding: 6px;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
input,
|
||||
textarea,
|
||||
.control {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -434,7 +329,7 @@ input[type='text'] {
|
||||
background-position: center;
|
||||
transition: -webkit-filter 0.1s ease-out;
|
||||
position: relative;
|
||||
animation: fadein .4s;
|
||||
animation: fadein 0.5s;
|
||||
}
|
||||
|
||||
.torrent,
|
||||
@@ -443,26 +338,18 @@ input[type='text'] {
|
||||
}
|
||||
|
||||
.torrent:not(:last-child) {
|
||||
border-bottom: 1px solid rgb(20, 20, 20);
|
||||
}
|
||||
|
||||
.torrent:hover {
|
||||
-webkit-filter: brightness(1.1);
|
||||
border-bottom: 1px solid #282828;
|
||||
}
|
||||
|
||||
.torrent .metadata {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
top: 20px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
width: calc(100% - 40px);
|
||||
width: calc(100% - 30px);
|
||||
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
|
||||
}
|
||||
|
||||
.torrent:hover .metadata {
|
||||
width: calc(100% - 150px);
|
||||
}
|
||||
|
||||
.torrent .metadata span:not(:last-child)::after {
|
||||
content: ' • ';
|
||||
opacity: 0.7;
|
||||
@@ -470,79 +357,34 @@ input[type='text'] {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.torrent .buttons {
|
||||
position: absolute;
|
||||
top: 29px;
|
||||
right: 10px;
|
||||
align-items: center;
|
||||
.torrent:hover .metadata {
|
||||
width: calc(100% - 120px);
|
||||
}
|
||||
|
||||
.torrent .torrent-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.torrent:hover .buttons {
|
||||
display: flex;
|
||||
.torrent:hover .torrent-controls {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.torrent .buttons > * {
|
||||
margin-left: 6px; /* space buttons apart */
|
||||
.torrent .play {
|
||||
position: absolute;
|
||||
top: 23px;
|
||||
right: 45px;
|
||||
font-size: 55px;
|
||||
}
|
||||
|
||||
.torrent .buttons .download {
|
||||
background-color: #2233BB;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 14px;
|
||||
font-size: 18px;
|
||||
padding-top: 6px;
|
||||
.torrent .delete {
|
||||
position: absolute;
|
||||
top: 38px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.torrent .buttons .download.downloading {
|
||||
color: #44dd44;
|
||||
}
|
||||
|
||||
.torrent .buttons .download.seeding {
|
||||
color: #44dd44;
|
||||
}
|
||||
|
||||
.torrent .buttons .play {
|
||||
padding-top: 10px;
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
.torrent.timeout .play {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.torrent.requested .play,
|
||||
.loading-spinner {
|
||||
border-top: 6px solid rgba(255, 255, 255, 0.2);
|
||||
border-right: 6px solid rgba(255, 255, 255, 0.2);
|
||||
border-bottom: 6px solid rgba(255, 255, 255, 0.2);
|
||||
border-left: 6px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
color: transparent;
|
||||
animation: load8 1.1s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes load8 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.torrent .buttons .play.resume-position {
|
||||
position: relative;
|
||||
-webkit-clip-path: circle(18px at center);
|
||||
}
|
||||
|
||||
.torrent .buttons .delete {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.torrent .buttons .delete:hover {
|
||||
opacity: 0.7;
|
||||
.torrent .download {
|
||||
vertical-align: -0.4em;
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.torrent .buttons .radial-progress {
|
||||
@@ -550,9 +392,27 @@ input[type='text'] {
|
||||
}
|
||||
|
||||
.torrent .name {
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
line-height: 1.5em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.torrent hr {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: calc(50% - 20px);
|
||||
width: 40px;
|
||||
height: 1px;
|
||||
background: #ccc;
|
||||
display: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.torrent:hover hr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -606,6 +466,7 @@ body.drag .app::after {
|
||||
/*
|
||||
* TORRENT LIST: EXPANDED TORRENT DETAILS
|
||||
*/
|
||||
|
||||
.torrent.selected {
|
||||
height: auto;
|
||||
}
|
||||
@@ -629,10 +490,6 @@ body.drag .app::after {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.torrent-details tr:hover {
|
||||
background-color: rgba(200, 200, 200, 0.3);
|
||||
}
|
||||
|
||||
.torrent-details td {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
@@ -729,11 +586,15 @@ body.drag .app::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.player .controls .play-pause {
|
||||
.player .controls .icon.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.player .controls .icon.skip-previous,
|
||||
.player .controls .icon.play-pause,
|
||||
.player .controls .icon.skip-next {
|
||||
font-size: 28px;
|
||||
margin-top: 5px;
|
||||
margin-right: 10px;
|
||||
margin-left: 15px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.player .controls .volume-slider {
|
||||
@@ -775,14 +636,6 @@ body.drag .app::after {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
/* TODO: find better way to handle this (that also
|
||||
* keeps the header visible too).
|
||||
*/
|
||||
.app.hide-video-controls .player .controls:hover {
|
||||
opacity: 1;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* invisible click target for scrubbing */
|
||||
.player .controls .scrub-bar {
|
||||
position: absolute;
|
||||
@@ -917,185 +770,6 @@ video::-webkit-media-text-track-container {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 .icon.enabled {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.preferences .checkbox {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* MEDIA OVERLAY / AUDIO DETAILS
|
||||
*/
|
||||
@@ -1254,3 +928,19 @@ video::-webkit-media-text-track-container {
|
||||
height: 32px;
|
||||
margin: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this class on Material UI components to get correct native app behavior:
|
||||
*
|
||||
* - Dragging the button should NOT drag the entire app window
|
||||
* - The cursor should be default, not a pointer (hand) like on the web
|
||||
*/
|
||||
|
||||
.control {
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.control * {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WebTorrent Desktop</title>
|
||||
<title>Main Window</title>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- React prints a warning if you render to <body> directly -->
|
||||
<div id='body'></div>
|
||||
<!-- We can't just say src='...main.js', that breaks require()s -->
|
||||
<script>
|
||||
require('../build/renderer/main.js')
|
||||
</script>
|
||||
<script>require('../build/renderer/main.js')</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WebTorrent Desktop</title>
|
||||
<title>WebTorrent Hidden Window</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #282828;
|
||||
|
||||
15
test/config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const path = require('path')
|
||||
|
||||
const TEMP_DIR = process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp'
|
||||
const TEST_DIR = path.join(TEMP_DIR, 'WebTorrentTest')
|
||||
const TEST_DIR_DOWNLOAD = path.join(TEST_DIR, 'Downloads')
|
||||
const TEST_DIR_DESKTOP = path.join(TEST_DIR, 'Desktop')
|
||||
|
||||
module.exports = {
|
||||
TORRENT_FILES: [path.join(__dirname, 'resources', '1.torrent')],
|
||||
SEED_FILES: [path.join(TEST_DIR_DESKTOP, 'tmp.jpg')],
|
||||
SAVED_TORRENT_FILE: path.join(TEST_DIR_DESKTOP, 'saved.torrent'),
|
||||
TEST_DIR,
|
||||
TEST_DIR_DOWNLOAD,
|
||||
TEST_DIR_DESKTOP
|
||||
}
|
||||
19
test/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const test = require('tape')
|
||||
const setup = require('./setup')
|
||||
|
||||
test.onFinish(setup.deleteTestDataDir)
|
||||
|
||||
test('app runs', function (t) {
|
||||
t.timeoutAfter(10e3)
|
||||
setup.resetTestDataDir()
|
||||
const app = setup.createApp()
|
||||
setup.waitForLoad(app, t)
|
||||
.then(() => setup.screenshotCreateOrCompare(app, t, 'app-basic'))
|
||||
.then(() => setup.endTest(app, t),
|
||||
(err) => setup.endTest(app, t, err || 'error'))
|
||||
})
|
||||
|
||||
require('./test-torrent-list')
|
||||
require('./test-add-torrent')
|
||||
require('./test-video')
|
||||
require('./test-audio')
|
||||
15
test/mocks.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const electron = require('electron')
|
||||
const config = require('./config')
|
||||
|
||||
console.log('Mocking electron.dialog.showOpenDialog...')
|
||||
electron.dialog.showOpenDialog = function (win, opts, cb) {
|
||||
const ret = /select.*torrent file/i.test(opts.title)
|
||||
? config.TORRENT_FILES
|
||||
: config.SEED_FILES
|
||||
cb(ret)
|
||||
}
|
||||
|
||||
console.log('Mocking electron.remote.dialog.showSaveDialog...')
|
||||
electron.dialog.showSaveDialog = function (win, opts, cb) {
|
||||
cb(config.SAVED_TORRENT_FILE)
|
||||
}
|
||||