Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
f259b32cce | ||
|
|
eba9aa3e17 | ||
|
|
905eb1611e | ||
|
|
6d4b8c3c26 | ||
|
|
6a46609cca | ||
|
|
e872282221 | ||
|
|
24ac5af5b4 | ||
|
|
0ee92fb632 | ||
|
|
7cbc12c6ff | ||
|
|
60c82c73cd | ||
|
|
78790e73c7 | ||
|
|
bf464de16f | ||
|
|
0589963eed | ||
|
|
b79971eea5 | ||
|
|
d1e557f054 | ||
|
|
93ddb8d638 | ||
|
|
06fdd80845 | ||
|
|
b0b26f8300 | ||
|
|
1db890f5e7 | ||
|
|
0f80f96023 | ||
|
|
2d3673ea33 | ||
|
|
c28260611e | ||
|
|
b5dd00007a | ||
|
|
ac39264f3d | ||
|
|
667a04a41d | ||
|
|
51a9b2ea9b | ||
|
|
842ee5ca3c | ||
|
|
2cc67dbda7 | ||
|
|
70bc32614b | ||
|
|
9bf44d7d7e | ||
|
|
f48ecb87b2 | ||
|
|
1765aba681 | ||
|
|
c6063c759e | ||
|
|
bb4db2cede | ||
|
|
7c36898f78 | ||
|
|
23e8cdf216 | ||
|
|
5ffd4123a1 | ||
|
|
27e3c14f10 | ||
|
|
d57bfb825a | ||
|
|
0809e20a6e | ||
|
|
1ec305162e | ||
|
|
45d46d7ae8 | ||
|
|
adb41736d5 | ||
|
|
09d6fa550a | ||
|
|
75cc7383cb | ||
|
|
4d48b9e7c1 | ||
|
|
563e1ca0ba | ||
|
|
0fa3b678b0 | ||
|
|
8420c65d25 | ||
|
|
3232e96f6e | ||
|
|
110e25af73 | ||
|
|
8233faf518 | ||
|
|
39ae0343fc | ||
|
|
c637878603 | ||
|
|
91e61f6cd4 | ||
|
|
9f66418073 | ||
|
|
2c3d667675 | ||
|
|
6c68645b0f | ||
|
|
dc7ccb3956 | ||
|
|
a420936657 | ||
|
|
dcab7f72d4 | ||
|
|
a695f7c2d7 | ||
|
|
7677bff6d4 | ||
|
|
c7626997de | ||
|
|
91a1ab4a56 | ||
|
|
3e19cdfb0b | ||
|
|
2043dc2161 | ||
|
|
a9e36472c5 | ||
|
|
4df4f9b2ad | ||
|
|
4ad55173a5 | ||
|
|
b9c82dd6b2 | ||
|
|
8333f4893f | ||
|
|
f071965ae8 | ||
|
|
a4fa9ac666 | ||
|
|
939ee555b7 |
7
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules/
|
||||||
build
|
build/
|
||||||
dist
|
dist/
|
||||||
|
npm-debug.log.*
|
||||||
|
|||||||
2
.nodemonignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
build/
|
||||||
|
dist/
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 'node'
|
- 'node'
|
||||||
install: npm install standard
|
install: npm install standard depcheck walk-sync
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
- Romain Beaumont (romain.rom1@gmail.com)
|
- Romain Beaumont (romain.rom1@gmail.com)
|
||||||
- Dan Flettre (fletd01@yahoo.com)
|
- Dan Flettre (fletd01@yahoo.com)
|
||||||
- Liam Gray (liam.r.gray@gmail.com)
|
- Liam Gray (liam.r.gray@gmail.com)
|
||||||
- grunjol (grunjol@users.noreply.github.com)
|
|
||||||
- Rémi Jouannet (remijouannet@users.noreply.github.com)
|
- Rémi Jouannet (remijouannet@users.noreply.github.com)
|
||||||
- Evan Miller (miller.evan815@gmail.com)
|
- Evan Miller (miller.evan815@gmail.com)
|
||||||
- Alex (alxmorais8@msn.com)
|
- Alex (alxmorais8@msn.com)
|
||||||
@@ -24,8 +23,14 @@
|
|||||||
- Thomas Watson Steen (w@tson.dk)
|
- Thomas Watson Steen (w@tson.dk)
|
||||||
- anonymlol (anonymlol7@gmail.com)
|
- anonymlol (anonymlol7@gmail.com)
|
||||||
- Gediminas Petrikas (gedas18@gmail.com)
|
- Gediminas Petrikas (gedas18@gmail.com)
|
||||||
|
- Alberto Miranda (codealchemist@gmail.com)
|
||||||
- Adam Gotlib (gotlib.adam+dev@gmail.com)
|
- Adam Gotlib (gotlib.adam+dev@gmail.com)
|
||||||
- Rémi Jouannet (remijouannet@gmail.com)
|
- Rémi Jouannet (remijouannet@gmail.com)
|
||||||
- Andrea Tupini (tupini07@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)
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
72
CHANGELOG.md
@@ -1,5 +1,77 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Do not automatically set WebTorrent as the default handler for torrents (#771)
|
||||||
|
- Torrents can only be created from the home screen (#770)
|
||||||
|
- Update Electron to 1.3.3 (#772)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Allow modifying the default tracker list on the Create Torrent page (#775)
|
||||||
|
- Prevent opening multiple stacked Preference windows or Create Torrent windows (#770)
|
||||||
|
- Windows: Player window auto-resize does not match video aspect ratio (#565)
|
||||||
|
- Missing page title on Create Torrent page
|
||||||
|
|
||||||
## v0.10.0 - 2016-08-05
|
## v0.10.0 - 2016-08-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
58
README.md
@@ -7,7 +7,7 @@
|
|||||||
<br>
|
<br>
|
||||||
</h1>
|
</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">
|
<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>
|
<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,46 @@
|
|||||||
|
|
||||||
## Install
|
## 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">
|
<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">
|
<img src="https://webtorrent.io/img/screenshot-main.png" width="612" height="749" alt="screenshot" align="center">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## How to Contribute
|
## How to Contribute
|
||||||
|
|
||||||
### Install dependencies
|
### Get the code
|
||||||
|
|
||||||
```
|
```
|
||||||
|
$ git clone https://github.com/feross/webtorrent-desktop.git
|
||||||
|
$ cd webtorrent-desktop
|
||||||
$ npm install
|
$ npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run app
|
### Run the app
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm start
|
$ npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Package app
|
### Watch the code
|
||||||
|
|
||||||
|
Restart the app automatically every time code changes. Useful during development.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run watch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package the app
|
||||||
|
|
||||||
Builds app binaries for Mac, Linux, and Windows.
|
Builds app binaries for Mac, Linux, and Windows.
|
||||||
|
|
||||||
@@ -50,7 +67,7 @@ $ npm run package
|
|||||||
To build for one platform:
|
To build for one platform:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm run package -- [platform]
|
$ npm run package -- [platform] [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default).
|
Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default).
|
||||||
@@ -66,14 +83,18 @@ The following optional arguments are available:
|
|||||||
- `portable` - Windows portable app
|
- `portable` - Windows portable app
|
||||||
- `all` - All platforms (default)
|
- `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
|
#### Windows build notes
|
||||||
|
|
||||||
To package the Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs
|
The Windows app can be packaged from **any** platform.
|
||||||
to be installed.
|
|
||||||
|
|
||||||
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
|
brew install wine
|
||||||
@@ -81,11 +102,22 @@ brew install wine
|
|||||||
|
|
||||||
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
(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
|
### 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
|
### 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
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
var electron = require('electron-prebuilt')
|
var electron = require('electron')
|
||||||
var cp = require('child_process')
|
var cp = require('child_process')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
var files = walkSync('src', {globs: ['**/*.js']})
|
||||||
|
console.log('Running extra-lint on ' + files.length + ' files...')
|
||||||
|
|
||||||
|
// Read each file, line by line
|
||||||
|
files.forEach(function (file) {
|
||||||
|
var filepath = path.join('src', file)
|
||||||
|
var lines = fs.readFileSync(filepath, 'utf8').split('\n')
|
||||||
|
|
||||||
|
lines.forEach(function (line, i) {
|
||||||
|
var 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
|
|
||||||
@@ -105,7 +105,7 @@ var all = {
|
|||||||
prune: true,
|
prune: true,
|
||||||
|
|
||||||
// The Electron version with which the app is built (without the leading 'v')
|
// The Electron version with which the app is built (without the leading 'v')
|
||||||
version: require('electron-prebuilt/package.json').version
|
version: require('electron/package.json').version
|
||||||
}
|
}
|
||||||
|
|
||||||
var darwin = {
|
var darwin = {
|
||||||
@@ -484,6 +484,11 @@ function buildLinux (cb) {
|
|||||||
dest: destPath,
|
dest: destPath,
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: filesPath
|
cwd: filesPath
|
||||||
|
}, {
|
||||||
|
src: ['./**'],
|
||||||
|
dest: path.join('/usr', 'share'),
|
||||||
|
expand: true,
|
||||||
|
cwd: path.join(config.STATIC_PATH, 'linux', 'share')
|
||||||
}], function (err) {
|
}], function (err) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log(`Linux: Created ${destArch} deb.`)
|
console.log(`Linux: Created ${destArch} deb.`)
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ npm run package -- --sign
|
|||||||
git push
|
git push
|
||||||
git push --tags
|
git push --tags
|
||||||
npm publish
|
npm publish
|
||||||
./node_modules/.bin/gh-release
|
npm run gh-release
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ while (<>) {
|
|||||||
next if /(ungoldman\@gmail.com)/;
|
next if /(ungoldman\@gmail.com)/;
|
||||||
next if /(dc\@DCs-MacBook.local)/;
|
next if /(dc\@DCs-MacBook.local)/;
|
||||||
next if /(rolandoguedes\@gmail.com)/;
|
next if /(rolandoguedes\@gmail.com)/;
|
||||||
|
next if /(grunjol\@users.noreply.github.com)/;
|
||||||
$seen{$_} = push @authors, "- ", $_;
|
$seen{$_} = push @authors, "- ", $_;
|
||||||
}
|
}
|
||||||
END {
|
END {
|
||||||
|
|||||||
22
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||||
"version": "0.10.0",
|
"version": "0.14.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@webtorrent.io",
|
"email": "feross@webtorrent.io",
|
||||||
@@ -22,18 +22,19 @@
|
|||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.1.0",
|
"dlnacasts": "^0.1.0",
|
||||||
"drag-drop": "^2.12.1",
|
"drag-drop": "^2.12.1",
|
||||||
"electron-prebuilt": "1.3.2",
|
"electron": "1.3.3",
|
||||||
"fs-extra": "^0.30.0",
|
"fs-extra": "^0.30.0",
|
||||||
"hat": "0.0.3",
|
|
||||||
"iso-639-1": "^1.2.1",
|
"iso-639-1": "^1.2.1",
|
||||||
"languagedetect": "^1.1.1",
|
"languagedetect": "^1.1.1",
|
||||||
"location-history": "^1.0.0",
|
"location-history": "^1.0.0",
|
||||||
|
"material-ui": "^0.15.4",
|
||||||
"musicmetadata": "^2.0.2",
|
"musicmetadata": "^2.0.2",
|
||||||
"network-address": "^1.1.0",
|
"network-address": "^1.1.0",
|
||||||
"parse-torrent": "^5.7.3",
|
"parse-torrent": "^5.7.3",
|
||||||
"prettier-bytes": "^1.0.1",
|
"prettier-bytes": "^1.0.1",
|
||||||
"react": "^15.2.1",
|
"react": "^15.2.1",
|
||||||
"react-dom": "^15.2.1",
|
"react-dom": "^15.2.1",
|
||||||
|
"react-tap-event-plugin": "^1.0.0",
|
||||||
"run-parallel": "^1.1.6",
|
"run-parallel": "^1.1.6",
|
||||||
"semver": "^5.1.0",
|
"semver": "^5.1.0",
|
||||||
"simple-concat": "^1.0.0",
|
"simple-concat": "^1.0.0",
|
||||||
@@ -47,8 +48,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.11.4",
|
"babel-cli": "^6.11.4",
|
||||||
"babel-plugin-syntax-jsx": "^6.13.0",
|
"babel-plugin-syntax-jsx": "^6.13.0",
|
||||||
|
"babel-plugin-transform-es2015-destructuring": "^6.9.0",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||||
"babel-plugin-transform-react-jsx": "^6.8.0",
|
"babel-plugin-transform-react-jsx": "^6.8.0",
|
||||||
"cross-zip": "^2.0.1",
|
"cross-zip": "^2.0.1",
|
||||||
|
"depcheck": "^0.6.4",
|
||||||
"electron-osx-sign": "^0.3.0",
|
"electron-osx-sign": "^0.3.0",
|
||||||
"electron-packager": "^7.0.0",
|
"electron-packager": "^7.0.0",
|
||||||
"electron-winstaller": "^2.3.0",
|
"electron-winstaller": "^2.3.0",
|
||||||
@@ -56,11 +60,13 @@
|
|||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"nobin-debian-installer": "^0.0.10",
|
"nobin-debian-installer": "^0.0.10",
|
||||||
|
"nodemon": "^1.10.2",
|
||||||
"open": "0.0.5",
|
"open": "0.0.5",
|
||||||
"plist": "^1.2.0",
|
"plist": "^2.0.1",
|
||||||
"rimraf": "^2.5.2",
|
"rimraf": "^2.5.2",
|
||||||
"run-series": "^1.1.4",
|
"run-series": "^1.1.4",
|
||||||
"standard": "^7.0.0"
|
"standard": "*",
|
||||||
|
"walk-sync": "^0.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0.0"
|
"node": ">=4.0.0"
|
||||||
@@ -93,7 +99,9 @@
|
|||||||
"package": "node ./bin/package.js",
|
"package": "node ./bin/package.js",
|
||||||
"prepublish": "npm run build",
|
"prepublish": "npm run build",
|
||||||
"start": "npm run build && electron .",
|
"start": "npm run build && electron .",
|
||||||
"test": "standard && node ./bin/check-deps.js",
|
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
|
||||||
"update-authors": "./bin/update-authors.sh"
|
"gh-release": "gh-release",
|
||||||
|
"update-authors": "./bin/update-authors.sh",
|
||||||
|
"watch": "nodemon --exec \"npm run start\" --ext js,pug,css"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"syntax-jsx",
|
"syntax-jsx",
|
||||||
|
"transform-es2015-destructuring",
|
||||||
|
"transform-object-rest-spread",
|
||||||
"transform-react-jsx"
|
"transform-react-jsx"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
var appConfig = require('application-config')('WebTorrent')
|
const appConfig = require('application-config')('WebTorrent')
|
||||||
var fs = require('fs')
|
const fs = require('fs')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
var APP_NAME = 'WebTorrent'
|
const APP_NAME = 'WebTorrent'
|
||||||
var APP_TEAM = 'WebTorrent, LLC'
|
const APP_TEAM = 'WebTorrent, LLC'
|
||||||
var APP_VERSION = require('../package.json').version
|
const APP_VERSION = require('../package.json').version
|
||||||
|
|
||||||
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
const PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||||
@@ -95,7 +95,7 @@ function getDefaultDownloadPath () {
|
|||||||
return path.join(getConfigPath(), 'Downloads')
|
return path.join(getConfigPath(), 'Downloads')
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
return process.type === 'renderer'
|
return process.type === 'renderer'
|
||||||
? electron.remote.app.getPath('downloads')
|
? electron.remote.app.getPath('downloads')
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ module.exports = {
|
|||||||
init
|
init
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = require('./config')
|
const config = require('./config')
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
electron.crashReporter.start({
|
electron.crashReporter.start({
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ module.exports = {
|
|||||||
init
|
init
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
|
|
||||||
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
const ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
||||||
'?version=' + config.APP_VERSION +
|
'?version=' + config.APP_VERSION +
|
||||||
'&platform=' + process.platform
|
'&platform=' + process.platform
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
function init () {
|
function init () {
|
||||||
var get = require('simple-get')
|
const get = require('simple-get')
|
||||||
get.concat(ANNOUNCEMENT_URL, onResponse)
|
get.concat(ANNOUNCEMENT_URL, onResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ module.exports = {
|
|||||||
openFiles
|
openFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show open dialog to create a single-file torrent.
|
* Show open dialog to create a single-file torrent.
|
||||||
@@ -17,7 +17,7 @@ var windows = require('./windows')
|
|||||||
function openSeedFile () {
|
function openSeedFile () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openSeedFile')
|
log('openSeedFile')
|
||||||
var opts = {
|
const opts = {
|
||||||
title: 'Select a file for the torrent.',
|
title: 'Select a file for the torrent.',
|
||||||
properties: [ 'openFile' ]
|
properties: [ 'openFile' ]
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ function openSeedFile () {
|
|||||||
function openSeedDirectory () {
|
function openSeedDirectory () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openSeedDirectory')
|
log('openSeedDirectory')
|
||||||
var opts = process.platform === 'darwin'
|
const opts = process.platform === 'darwin'
|
||||||
? {
|
? {
|
||||||
title: 'Select a file or folder for the torrent.',
|
title: 'Select a file or folder for the torrent.',
|
||||||
properties: [ 'openFile', 'openDirectory' ]
|
properties: [ 'openFile', 'openDirectory' ]
|
||||||
@@ -61,7 +61,7 @@ function openSeedDirectory () {
|
|||||||
function openFiles () {
|
function openFiles () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openFiles')
|
log('openFiles')
|
||||||
var opts = process.platform === 'darwin'
|
const opts = process.platform === 'darwin'
|
||||||
? {
|
? {
|
||||||
title: 'Select a file or folder to add.',
|
title: 'Select a file or folder to add.',
|
||||||
properties: [ 'openFile', 'openDirectory' ]
|
properties: [ 'openFile', 'openDirectory' ]
|
||||||
@@ -84,7 +84,7 @@ function openFiles () {
|
|||||||
function openTorrentFile () {
|
function openTorrentFile () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openTorrentFile')
|
log('openTorrentFile')
|
||||||
var opts = {
|
const opts = {
|
||||||
title: 'Select a .torrent file.',
|
title: 'Select a .torrent file.',
|
||||||
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
||||||
properties: [ 'openFile', 'multiSelections' ]
|
properties: [ 'openFile', 'multiSelections' ]
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ module.exports = {
|
|||||||
setBadge
|
setBadge
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const {app, Menu} = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const dialog = require('./dialog')
|
||||||
|
const log = require('./log')
|
||||||
var dialog = require('./dialog')
|
|
||||||
var log = require('./log')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a right-click menu to the dock icon. (Mac)
|
* Add a right-click menu to the dock icon. (Mac)
|
||||||
*/
|
*/
|
||||||
function init () {
|
function init () {
|
||||||
if (!app.dock) return
|
if (!app.dock) return
|
||||||
var menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
const menu = Menu.buildFromTemplate(getMenuTemplate())
|
||||||
app.dock.setMenu(menu)
|
app.dock.setMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +31,11 @@ function downloadFinished (path) {
|
|||||||
* Display a counter badge for the app. (Mac, Linux)
|
* Display a counter badge for the app. (Mac, Linux)
|
||||||
*/
|
*/
|
||||||
function setBadge (count) {
|
function setBadge (count) {
|
||||||
log(`setBadge: ${count}`)
|
if (process.platform === 'darwin' ||
|
||||||
app.setBadgeCount(Number(count))
|
process.platform === 'linux' && app.isUnityRunning()) {
|
||||||
|
log(`setBadge: ${count}`)
|
||||||
|
app.setBadgeCount(Number(count))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMenuTemplate () {
|
function getMenuTemplate () {
|
||||||
|
|||||||
71
src/main/external-player.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
module.exports = {
|
||||||
|
spawn,
|
||||||
|
kill,
|
||||||
|
checkInstall
|
||||||
|
}
|
||||||
|
|
||||||
|
const cp = require('child_process')
|
||||||
|
const vlcCommand = require('vlc-command')
|
||||||
|
|
||||||
|
const log = require('./log')
|
||||||
|
const windows = require('./windows')
|
||||||
|
|
||||||
|
// holds a ChildProcess while we're playing a video in an external player, null otherwise
|
||||||
|
let proc = null
|
||||||
|
|
||||||
|
function checkInstall (path, 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))
|
||||||
|
process.nextTick(() => cb(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawn (path, url, title) {
|
||||||
|
if (path != null) return spawnExternal(path, [url])
|
||||||
|
|
||||||
|
// Try to find and use VLC if external player is not specified
|
||||||
|
vlcCommand(function (err, vlcPath) {
|
||||||
|
if (err) return windows.main.dispatch('externalPlayerNotFound')
|
||||||
|
const args = [
|
||||||
|
'--play-and-exit',
|
||||||
|
'--video-on-top',
|
||||||
|
'--quiet',
|
||||||
|
`--meta-title=${JSON.stringify(title)}`,
|
||||||
|
url
|
||||||
|
]
|
||||||
|
spawnExternal(vlcPath, args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function kill () {
|
||||||
|
if (!proc) return
|
||||||
|
log('Killing external player, pid ' + proc.pid)
|
||||||
|
proc.kill('SIGKILL') // kill -9
|
||||||
|
proc = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnExternal (path, args) {
|
||||||
|
log('Running external media player:', path + ' ' + args.join(' '))
|
||||||
|
|
||||||
|
proc = cp.spawn(path, args, {stdio: 'ignore'})
|
||||||
|
|
||||||
|
// If it works, close the modal after a second
|
||||||
|
const closeModalTimeout = setTimeout(() =>
|
||||||
|
windows.main.dispatch('exitModal'), 1000)
|
||||||
|
|
||||||
|
proc.on('close', function (code) {
|
||||||
|
clearTimeout(closeModalTimeout)
|
||||||
|
if (!proc) return // Killed
|
||||||
|
log('External player exited with code ', code)
|
||||||
|
if (code === 0) {
|
||||||
|
windows.main.dispatch('backToList')
|
||||||
|
} else {
|
||||||
|
windows.main.dispatch('externalPlayerNotFound')
|
||||||
|
}
|
||||||
|
proc = null
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on('error', function (e) {
|
||||||
|
log('External player error', e)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ module.exports = {
|
|||||||
uninstall
|
uninstall
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
function install () {
|
function install () {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
@@ -31,8 +31,8 @@ function uninstall () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function installDarwin () {
|
function installDarwin () {
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
// On Mac, only protocols that are listed in `Info.plist` can be set as the
|
// On Mac, only protocols that are listed in `Info.plist` can be set as the
|
||||||
// default handler at runtime.
|
// default handler at runtime.
|
||||||
@@ -44,18 +44,18 @@ function installDarwin () {
|
|||||||
|
|
||||||
function uninstallDarwin () {}
|
function uninstallDarwin () {}
|
||||||
|
|
||||||
var EXEC_COMMAND = [ process.execPath ]
|
const EXEC_COMMAND = [ process.execPath ]
|
||||||
|
|
||||||
if (!config.IS_PRODUCTION) {
|
if (!config.IS_PRODUCTION) {
|
||||||
EXEC_COMMAND.push(config.ROOT_PATH)
|
EXEC_COMMAND.push(config.ROOT_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
function installWin32 () {
|
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'
|
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
|
||||||
)
|
)
|
||||||
registerProtocolHandlerWin32(
|
registerProtocolHandlerWin32(
|
||||||
@@ -100,7 +100,7 @@ function installWin32 () {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function registerProtocolHandlerWin32 (protocol, name, icon, command) {
|
function registerProtocolHandlerWin32 (protocol, name, icon, command) {
|
||||||
var protocolKey = new Registry({
|
const protocolKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + protocol
|
key: '\\Software\\Classes\\' + protocol
|
||||||
})
|
})
|
||||||
@@ -120,7 +120,7 @@ function installWin32 () {
|
|||||||
function setIcon (err) {
|
function setIcon (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) log.error(err.message)
|
||||||
|
|
||||||
var iconKey = new Registry({
|
const iconKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
|
key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
|
||||||
})
|
})
|
||||||
@@ -130,7 +130,7 @@ function installWin32 () {
|
|||||||
function setCommand (err) {
|
function setCommand (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) log.error(err.message)
|
||||||
|
|
||||||
var commandKey = new Registry({
|
const commandKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
||||||
})
|
})
|
||||||
@@ -161,7 +161,7 @@ function installWin32 () {
|
|||||||
setExt()
|
setExt()
|
||||||
|
|
||||||
function setExt () {
|
function setExt () {
|
||||||
var extKey = new Registry({
|
const extKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + ext
|
key: '\\Software\\Classes\\' + ext
|
||||||
})
|
})
|
||||||
@@ -171,7 +171,7 @@ function installWin32 () {
|
|||||||
function setId (err) {
|
function setId (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) log.error(err.message)
|
||||||
|
|
||||||
var idKey = new Registry({
|
const idKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + id
|
key: '\\Software\\Classes\\' + id
|
||||||
})
|
})
|
||||||
@@ -181,7 +181,7 @@ function installWin32 () {
|
|||||||
function setIcon (err) {
|
function setIcon (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) log.error(err.message)
|
||||||
|
|
||||||
var iconKey = new Registry({
|
const iconKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
|
key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
|
||||||
})
|
})
|
||||||
@@ -191,7 +191,7 @@ function installWin32 () {
|
|||||||
function setCommand (err) {
|
function setCommand (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) log.error(err.message)
|
||||||
|
|
||||||
var commandKey = new Registry({
|
const commandKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
|
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
|
||||||
})
|
})
|
||||||
@@ -205,7 +205,7 @@ function installWin32 () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function uninstallWin32 () {
|
function uninstallWin32 () {
|
||||||
var Registry = require('winreg')
|
const Registry = require('winreg')
|
||||||
|
|
||||||
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
||||||
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
|
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
|
||||||
@@ -215,7 +215,7 @@ function uninstallWin32 () {
|
|||||||
getCommand()
|
getCommand()
|
||||||
|
|
||||||
function getCommand () {
|
function getCommand () {
|
||||||
var commandKey = new Registry({
|
const commandKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
||||||
})
|
})
|
||||||
@@ -227,7 +227,7 @@ function uninstallWin32 () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function destroyProtocol () {
|
function destroyProtocol () {
|
||||||
var protocolKey = new Registry({
|
const protocolKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + protocol
|
key: '\\Software\\Classes\\' + protocol
|
||||||
})
|
})
|
||||||
@@ -239,7 +239,7 @@ function uninstallWin32 () {
|
|||||||
eraseId()
|
eraseId()
|
||||||
|
|
||||||
function eraseId () {
|
function eraseId () {
|
||||||
var idKey = new Registry({
|
const idKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + id
|
key: '\\Software\\Classes\\' + id
|
||||||
})
|
})
|
||||||
@@ -247,7 +247,7 @@ function uninstallWin32 () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getExt () {
|
function getExt () {
|
||||||
var extKey = new Registry({
|
const extKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + ext
|
key: '\\Software\\Classes\\' + ext
|
||||||
})
|
})
|
||||||
@@ -259,7 +259,7 @@ function uninstallWin32 () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function destroyExt () {
|
function destroyExt () {
|
||||||
var extKey = new Registry({
|
const extKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + ext
|
key: '\\Software\\Classes\\' + ext
|
||||||
})
|
})
|
||||||
@@ -273,18 +273,21 @@ function commandToArgs (command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function installLinux () {
|
function installLinux () {
|
||||||
var fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
var os = require('os')
|
const os = require('os')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
|
|
||||||
|
// Do not install in user dir if running on system
|
||||||
|
if (/^\/opt/.test(process.execPath)) return
|
||||||
|
|
||||||
installDesktopFile()
|
installDesktopFile()
|
||||||
installIconFile()
|
installIconFile()
|
||||||
|
|
||||||
function installDesktopFile () {
|
function installDesktopFile () {
|
||||||
var templatePath = path.join(
|
const templatePath = path.join(
|
||||||
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
|
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
|
||||||
)
|
)
|
||||||
fs.readFile(templatePath, 'utf8', writeDesktopFile)
|
fs.readFile(templatePath, 'utf8', writeDesktopFile)
|
||||||
@@ -293,7 +296,7 @@ function installLinux () {
|
|||||||
function writeDesktopFile (err, desktopFile) {
|
function writeDesktopFile (err, desktopFile) {
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var appPath = config.IS_PRODUCTION
|
const appPath = config.IS_PRODUCTION
|
||||||
? path.dirname(process.execPath)
|
? path.dirname(process.execPath)
|
||||||
: config.ROOT_PATH
|
: config.ROOT_PATH
|
||||||
|
|
||||||
@@ -302,7 +305,7 @@ function installLinux () {
|
|||||||
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
|
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
|
||||||
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
|
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
|
||||||
|
|
||||||
var desktopFilePath = path.join(
|
const desktopFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
'.local',
|
'.local',
|
||||||
'share',
|
'share',
|
||||||
@@ -316,14 +319,14 @@ function installLinux () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function installIconFile () {
|
function installIconFile () {
|
||||||
var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
|
const iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
|
||||||
fs.readFile(iconStaticPath, writeIconFile)
|
fs.readFile(iconStaticPath, writeIconFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeIconFile (err, iconFile) {
|
function writeIconFile (err, iconFile) {
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var iconFilePath = path.join(
|
const iconFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
'.local',
|
'.local',
|
||||||
'share',
|
'share',
|
||||||
@@ -338,11 +341,11 @@ function installLinux () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function uninstallLinux () {
|
function uninstallLinux () {
|
||||||
var os = require('os')
|
const os = require('os')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
|
|
||||||
var desktopFilePath = path.join(
|
const desktopFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
'.local',
|
'.local',
|
||||||
'share',
|
'share',
|
||||||
@@ -351,7 +354,7 @@ function uninstallLinux () {
|
|||||||
)
|
)
|
||||||
fs.removeSync(desktopFilePath)
|
fs.removeSync(desktopFilePath)
|
||||||
|
|
||||||
var iconFilePath = path.join(
|
const iconFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
'.local',
|
'.local',
|
||||||
'share',
|
'share',
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
var ipcMain = electron.ipcMain
|
const ipcMain = electron.ipcMain
|
||||||
|
|
||||||
var announcement = require('./announcement')
|
const announcement = require('./announcement')
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var crashReporter = require('../crash-reporter')
|
const crashReporter = require('../crash-reporter')
|
||||||
var dialog = require('./dialog')
|
const dialog = require('./dialog')
|
||||||
var dock = require('./dock')
|
const dock = require('./dock')
|
||||||
var handlers = require('./handlers')
|
const ipc = require('./ipc')
|
||||||
var ipc = require('./ipc')
|
const log = require('./log')
|
||||||
var log = require('./log')
|
const menu = require('./menu')
|
||||||
var menu = require('./menu')
|
const squirrelWin32 = require('./squirrel-win32')
|
||||||
var squirrelWin32 = require('./squirrel-win32')
|
const tray = require('./tray')
|
||||||
var tray = require('./tray')
|
const updater = require('./updater')
|
||||||
var updater = require('./updater')
|
const userTasks = require('./user-tasks')
|
||||||
var userTasks = require('./user-tasks')
|
const windows = require('./windows')
|
||||||
var windows = require('./windows')
|
|
||||||
|
|
||||||
var shouldQuit = false
|
let shouldQuit = false
|
||||||
var argv = sliceArgv(process.argv)
|
let argv = sliceArgv(process.argv)
|
||||||
|
|
||||||
if (config.IS_PRODUCTION) {
|
if (config.IS_PRODUCTION) {
|
||||||
// When Electron is running in production mode (packaged app), then run React
|
// When Electron is running in production mode (packaged app), then run React
|
||||||
@@ -35,8 +34,8 @@ if (process.platform === 'win32') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldQuit) {
|
if (!shouldQuit) {
|
||||||
// Prevent multiple instances of app from running at same time. New instances signal
|
// Prevent multiple instances of app from running at same time. New instances
|
||||||
// this instance and quit.
|
// signal this instance and quit.
|
||||||
shouldQuit = app.makeSingleInstance(onAppOpen)
|
shouldQuit = app.makeSingleInstance(onAppOpen)
|
||||||
if (shouldQuit) {
|
if (shouldQuit) {
|
||||||
app.quit()
|
app.quit()
|
||||||
@@ -52,7 +51,7 @@ function init () {
|
|||||||
app.setPath('userData', config.CONFIG_PATH)
|
app.setPath('userData', config.CONFIG_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isReady = false // app ready, windows can be created
|
let isReady = false // app ready, windows can be created
|
||||||
app.ipcReady = false // main window has finished loading and IPC is ready
|
app.ipcReady = false // main window has finished loading and IPC is ready
|
||||||
app.isQuitting = false
|
app.isQuitting = false
|
||||||
|
|
||||||
@@ -79,8 +78,8 @@ function init () {
|
|||||||
// Report uncaught exceptions
|
// Report uncaught exceptions
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
var errJSON = {message: err.message, stack: err.stack}
|
const error = {message: err.message, stack: err.stack}
|
||||||
windows.main.dispatch('uncaughtError', 'main', errJSON)
|
windows.main.dispatch('uncaughtError', 'main', error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -111,7 +110,6 @@ function init () {
|
|||||||
function delayedInit () {
|
function delayedInit () {
|
||||||
announcement.init()
|
announcement.init()
|
||||||
dock.init()
|
dock.init()
|
||||||
handlers.install()
|
|
||||||
tray.init()
|
tray.init()
|
||||||
updater.init()
|
updater.init()
|
||||||
userTasks.init()
|
userTasks.init()
|
||||||
@@ -150,7 +148,7 @@ function sliceArgv (argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function processArgv (argv) {
|
function processArgv (argv) {
|
||||||
var torrentIds = []
|
let torrentIds = []
|
||||||
argv.forEach(function (arg) {
|
argv.forEach(function (arg) {
|
||||||
if (arg === '-n') {
|
if (arg === '-n') {
|
||||||
dialog.openSeedDirectory()
|
dialog.openSeedDirectory()
|
||||||
@@ -161,7 +159,10 @@ function processArgv (argv) {
|
|||||||
} else if (arg.startsWith('-psn')) {
|
} else if (arg.startsWith('-psn')) {
|
||||||
// Ignore Mac launchd "process serial number" argument
|
// Ignore Mac launchd "process serial number" argument
|
||||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
||||||
} else {
|
} 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
|
||||||
|
// running.
|
||||||
torrentIds.push(arg)
|
torrentIds.push(arg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
101
src/main/ipc.js
@@ -2,29 +2,27 @@ module.exports = {
|
|||||||
init
|
init
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
var dialog = require('./dialog')
|
const dialog = require('./dialog')
|
||||||
var dock = require('./dock')
|
const dock = require('./dock')
|
||||||
var log = require('./log')
|
const handlers = require('./handlers')
|
||||||
var menu = require('./menu')
|
const log = require('./log')
|
||||||
var powerSaveBlocker = require('./power-save-blocker')
|
const menu = require('./menu')
|
||||||
var shell = require('./shell')
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
var shortcuts = require('./shortcuts')
|
const shell = require('./shell')
|
||||||
var vlc = require('./vlc')
|
const shortcuts = require('./shortcuts')
|
||||||
var windows = require('./windows')
|
const externalPlayer = require('./external-player')
|
||||||
var thumbar = require('./thumbar')
|
const windows = require('./windows')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||||
var messageQueueMainToWebTorrent = []
|
const messageQueueMainToWebTorrent = []
|
||||||
|
|
||||||
// holds a ChildProcess while we're playing a video in VLC, null otherwise
|
|
||||||
var vlcProcess
|
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
var ipc = electron.ipcMain
|
const ipc = electron.ipcMain
|
||||||
|
|
||||||
ipc.once('ipcReady', function (e) {
|
ipc.once('ipcReady', function (e) {
|
||||||
app.ipcReady = true
|
app.ipcReady = true
|
||||||
@@ -60,14 +58,19 @@ function init () {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('onPlayerOpen', function () {
|
ipc.on('onPlayerOpen', function () {
|
||||||
menu.onPlayerOpen()
|
menu.setPlayerOpen(true)
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
shortcuts.enable()
|
shortcuts.enable()
|
||||||
thumbar.enable()
|
thumbar.enable()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipc.on('onPlayerUpdate', function (e, ...args) {
|
||||||
|
menu.onPlayerUpdate(...args)
|
||||||
|
thumbar.onPlayerUpdate(...args)
|
||||||
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerClose', function () {
|
ipc.on('onPlayerClose', function () {
|
||||||
menu.onPlayerClose()
|
menu.setPlayerOpen(false)
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
shortcuts.disable()
|
shortcuts.disable()
|
||||||
thumbar.disable()
|
thumbar.disable()
|
||||||
@@ -91,11 +94,19 @@ function init () {
|
|||||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
||||||
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File handlers
|
||||||
|
*/
|
||||||
|
ipc.on('setDefaultFileHandler', (e, flag) => {
|
||||||
|
if (flag) handlers.install()
|
||||||
|
else handlers.uninstall()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Windows: Main
|
* Windows: Main
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var main = windows.main
|
const main = windows.main
|
||||||
|
|
||||||
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
|
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
|
||||||
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
|
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
|
||||||
@@ -103,57 +114,23 @@ function init () {
|
|||||||
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
||||||
ipc.on('show', () => main.show())
|
ipc.on('show', () => main.show())
|
||||||
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
||||||
|
ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VLC
|
* External Media Player
|
||||||
* TODO: Move most of this code to vlc.js
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('checkForVLC', function (e) {
|
ipc.on('checkForExternalPlayer', function (e, path) {
|
||||||
vlc.checkForVLC(function (isInstalled) {
|
externalPlayer.checkInstall(path, function (isInstalled) {
|
||||||
windows.main.send('checkForVLC', isInstalled)
|
windows.main.send('checkForExternalPlayer', isInstalled)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('vlcPlay', function (e, url, title) {
|
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
|
||||||
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', `--meta-title=${title}`, url]
|
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
||||||
log('Running vlc ' + args.join(' '))
|
|
||||||
|
|
||||||
vlc.spawn(args, function (err, proc) {
|
|
||||||
if (err) return windows.main.dispatch('vlcNotFound')
|
|
||||||
vlcProcess = proc
|
|
||||||
|
|
||||||
// If it works, close the modal after a second
|
|
||||||
var closeModalTimeout = setTimeout(() =>
|
|
||||||
windows.main.dispatch('exitModal'), 1000)
|
|
||||||
|
|
||||||
vlcProcess.on('close', function (code) {
|
|
||||||
clearTimeout(closeModalTimeout)
|
|
||||||
if (!vlcProcess) return // Killed
|
|
||||||
log('VLC exited with code ', code)
|
|
||||||
if (code === 0) {
|
|
||||||
windows.main.dispatch('backToList')
|
|
||||||
} else {
|
|
||||||
windows.main.dispatch('vlcNotFound')
|
|
||||||
}
|
|
||||||
vlcProcess = null
|
|
||||||
})
|
|
||||||
|
|
||||||
vlcProcess.on('error', function (e) {
|
|
||||||
log('VLC error', e)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
ipc.on('vlcQuit', function () {
|
|
||||||
if (!vlcProcess) return
|
|
||||||
log('Killing VLC, pid ' + vlcProcess.pid)
|
|
||||||
vlcProcess.kill('SIGKILL') // kill -9
|
|
||||||
vlcProcess = null
|
|
||||||
})
|
|
||||||
|
|
||||||
// Capture all events
|
// Capture all events
|
||||||
var oldEmit = ipc.emit
|
const oldEmit = ipc.emit
|
||||||
ipc.emit = function (name, e, ...args) {
|
ipc.emit = function (name, e, ...args) {
|
||||||
// Relay messages between the main window and the WebTorrent hidden window
|
// Relay messages between the main window and the WebTorrent hidden window
|
||||||
if (name.startsWith('wt-') && !app.isQuitting) {
|
if (name.startsWith('wt-') && !app.isQuitting) {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ module.exports.error = error
|
|||||||
* where they can be viewed in Developer Tools.
|
* where they can be viewed in Developer Tools.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
function log (...args) {
|
function log (...args) {
|
||||||
if (app.ipcReady) {
|
if (app.ipcReady) {
|
||||||
|
|||||||
127
src/main/menu.js
@@ -1,49 +1,66 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
onPlayerClose,
|
setPlayerOpen,
|
||||||
onPlayerOpen,
|
setWindowFocus,
|
||||||
|
setAllowNav,
|
||||||
|
onPlayerUpdate,
|
||||||
onToggleAlwaysOnTop,
|
onToggleAlwaysOnTop,
|
||||||
onToggleFullScreen,
|
onToggleFullScreen
|
||||||
onWindowBlur,
|
|
||||||
onWindowFocus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var dialog = require('./dialog')
|
const dialog = require('./dialog')
|
||||||
var shell = require('./shell')
|
const shell = require('./shell')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
var menu
|
let menu = null
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||||
electron.Menu.setApplicationMenu(menu)
|
electron.Menu.setApplicationMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function setPlayerOpen (flag) {
|
||||||
getMenuItem('Play/Pause').enabled = false
|
getMenuItem('Play/Pause').enabled = flag
|
||||||
getMenuItem('Increase Volume').enabled = false
|
getMenuItem('Skip Next').enabled = flag
|
||||||
getMenuItem('Decrease Volume').enabled = false
|
getMenuItem('Skip Previous').enabled = flag
|
||||||
getMenuItem('Step Forward').enabled = false
|
getMenuItem('Increase Volume').enabled = flag
|
||||||
getMenuItem('Step Backward').enabled = false
|
getMenuItem('Decrease Volume').enabled = flag
|
||||||
getMenuItem('Increase Speed').enabled = false
|
getMenuItem('Step Forward').enabled = flag
|
||||||
getMenuItem('Decrease Speed').enabled = false
|
getMenuItem('Step Backward').enabled = flag
|
||||||
getMenuItem('Add Subtitles File...').enabled = false
|
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 onPlayerOpen () {
|
function onPlayerUpdate (hasNext, hasPrevious) {
|
||||||
getMenuItem('Play/Pause').enabled = true
|
getMenuItem('Skip Next').enabled = hasNext
|
||||||
getMenuItem('Increase Volume').enabled = true
|
getMenuItem('Skip Previous').enabled = hasPrevious
|
||||||
getMenuItem('Decrease Volume').enabled = true
|
}
|
||||||
getMenuItem('Step Forward').enabled = true
|
|
||||||
getMenuItem('Step Backward').enabled = true
|
function setWindowFocus (flag) {
|
||||||
getMenuItem('Increase Speed').enabled = true
|
getMenuItem('Full Screen').enabled = flag
|
||||||
getMenuItem('Decrease Speed').enabled = true
|
getMenuItem('Float on Top').enabled = flag
|
||||||
getMenuItem('Add Subtitles File...').enabled = true
|
}
|
||||||
|
|
||||||
|
// Disallow opening more screens on top of the current one.
|
||||||
|
function setAllowNav (flag) {
|
||||||
|
getMenuItem('Preferences').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) {
|
function onToggleAlwaysOnTop (flag) {
|
||||||
@@ -54,19 +71,9 @@ function onToggleFullScreen (flag) {
|
|||||||
getMenuItem('Full Screen').checked = flag
|
getMenuItem('Full Screen').checked = flag
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
|
||||||
getMenuItem('Full Screen').enabled = false
|
|
||||||
getMenuItem('Float on Top').enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowFocus () {
|
|
||||||
getMenuItem('Full Screen').enabled = true
|
|
||||||
getMenuItem('Float on Top').enabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMenuItem (label) {
|
function getMenuItem (label) {
|
||||||
for (var i = 0; i < menu.items.length; i++) {
|
for (let i = 0; i < menu.items.length; i++) {
|
||||||
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
const menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||||
return item.label === label
|
return item.label === label
|
||||||
})
|
})
|
||||||
if (menuItem) return menuItem
|
if (menuItem) return menuItem
|
||||||
@@ -74,7 +81,7 @@ function getMenuItem (label) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMenuTemplate () {
|
function getMenuTemplate () {
|
||||||
var template = [
|
const template = [
|
||||||
{
|
{
|
||||||
label: 'File',
|
label: 'File',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -130,14 +137,6 @@ function getMenuTemplate () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Preferences',
|
|
||||||
accelerator: 'CmdOrCtrl+,',
|
|
||||||
click: () => windows.main.dispatch('preferences')
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -201,6 +200,21 @@ function getMenuTemplate () {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
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',
|
label: 'Increase Volume',
|
||||||
accelerator: 'CmdOrCtrl+Up',
|
accelerator: 'CmdOrCtrl+Up',
|
||||||
@@ -350,6 +364,17 @@ function getMenuTemplate () {
|
|||||||
click: () => dialog.openSeedFile()
|
click: () => dialog.openSeedFile()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Edit menu (Windows, Linux)
|
||||||
|
template[1].submenu.push(
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
accelerator: 'CmdOrCtrl+,',
|
||||||
|
click: () => windows.main.dispatch('preferences')
|
||||||
|
})
|
||||||
|
|
||||||
// Help menu (Windows, Linux)
|
// Help menu (Windows, Linux)
|
||||||
template[4].submenu.push(
|
template[4].submenu.push(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ module.exports = {
|
|||||||
disable
|
disable
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
|
|
||||||
var blockId = 0
|
let blockId = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block the system from entering low-power (sleep) mode or turning off the
|
* Block the system from entering low-power (sleep) mode or turning off the
|
||||||
* display.
|
* display.
|
||||||
*/
|
*/
|
||||||
function enable () {
|
function enable () {
|
||||||
disable() // Stop the previous power saver block, if one exists.
|
if (electron.powerSaveBlocker.isStarted(blockId)) {
|
||||||
|
// If a power saver block already exists, do nothing.
|
||||||
|
return
|
||||||
|
}
|
||||||
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||||
log(`powerSaveBlocker.enable: ${blockId}`)
|
log(`powerSaveBlocker.enable: ${blockId}`)
|
||||||
}
|
}
|
||||||
@@ -23,6 +26,7 @@ function enable () {
|
|||||||
*/
|
*/
|
||||||
function disable () {
|
function disable () {
|
||||||
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
||||||
|
// If a power saver block does not exist, do nothing.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
electron.powerSaveBlocker.stop(blockId)
|
electron.powerSaveBlocker.stop(blockId)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ module.exports = {
|
|||||||
moveItemToTrash
|
moveItemToTrash
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the given external protocol URL in the desktop’s default manner.
|
* Open the given external protocol URL in the desktop’s default manner.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ module.exports = {
|
|||||||
enable
|
enable
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
function enable () {
|
function enable () {
|
||||||
// Register play/pause media key, available on some keyboards.
|
// Register play/pause media key, available on some keyboards.
|
||||||
@@ -12,9 +12,19 @@ function enable () {
|
|||||||
'MediaPlayPause',
|
'MediaPlayPause',
|
||||||
() => windows.main.dispatch('playPause')
|
() => windows.main.dispatch('playPause')
|
||||||
)
|
)
|
||||||
|
electron.globalShortcut.register(
|
||||||
|
'MediaNextTrack',
|
||||||
|
() => windows.main.dispatch('nextTrack')
|
||||||
|
)
|
||||||
|
electron.globalShortcut.register(
|
||||||
|
'MediaPreviousTrack',
|
||||||
|
() => windows.main.dispatch('previousTrack')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function disable () {
|
function disable () {
|
||||||
// Return the media key to the OS, so other apps can use it.
|
// Return the media key to the OS, so other apps can use it.
|
||||||
electron.globalShortcut.unregister('MediaPlayPause')
|
electron.globalShortcut.unregister('MediaPlayPause')
|
||||||
|
electron.globalShortcut.unregister('MediaNextTrack')
|
||||||
|
electron.globalShortcut.unregister('MediaPreviousTrack')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ module.exports = {
|
|||||||
handleEvent
|
handleEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
var cp = require('child_process')
|
const cp = require('child_process')
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var fs = require('fs')
|
const fs = require('fs')
|
||||||
var os = require('os')
|
const os = require('os')
|
||||||
var path = require('path')
|
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)
|
const EXE_NAME = path.basename(process.execPath)
|
||||||
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||||
|
|
||||||
function handleEvent (cmd) {
|
function handleEvent (cmd) {
|
||||||
if (cmd === '--squirrel-install') {
|
if (cmd === '--squirrel-install') {
|
||||||
@@ -73,9 +73,9 @@ function handleEvent (cmd) {
|
|||||||
* the output from standard out.
|
* the output from standard out.
|
||||||
*/
|
*/
|
||||||
function spawn (command, args, cb) {
|
function spawn (command, args, cb) {
|
||||||
var stdout = ''
|
let stdout = ''
|
||||||
|
let error = null
|
||||||
var child
|
let child = null
|
||||||
try {
|
try {
|
||||||
child = cp.spawn(command, args)
|
child = cp.spawn(command, args)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -90,10 +90,10 @@ function spawn (command, args, cb) {
|
|||||||
stdout += data
|
stdout += data
|
||||||
})
|
})
|
||||||
|
|
||||||
var error = null
|
|
||||||
child.on('error', function (processError) {
|
child.on('error', function (processError) {
|
||||||
error = processError
|
error = processError
|
||||||
})
|
})
|
||||||
|
|
||||||
child.on('close', function (code, signal) {
|
child.on('close', function (code, signal) {
|
||||||
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
|
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
|
||||||
if (error) error.stdout = stdout
|
if (error) error.stdout = stdout
|
||||||
@@ -122,12 +122,12 @@ function createShortcuts (cb) {
|
|||||||
* command.
|
* command.
|
||||||
*/
|
*/
|
||||||
function updateShortcuts (cb) {
|
function updateShortcuts (cb) {
|
||||||
var homeDir = os.homedir()
|
const homeDir = os.homedir()
|
||||||
if (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.
|
// If the desktop shortcut was deleted by the user, then keep it deleted.
|
||||||
fs.access(desktopShortcutPath, function (err) {
|
fs.access(desktopShortcutPath, function (err) {
|
||||||
var desktopShortcutExists = !err
|
const desktopShortcutExists = !err
|
||||||
createShortcuts(function () {
|
createShortcuts(function () {
|
||||||
if (desktopShortcutExists) {
|
if (desktopShortcutExists) {
|
||||||
cb()
|
cb()
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ module.exports = {
|
|||||||
disable,
|
disable,
|
||||||
enable,
|
enable,
|
||||||
onPlayerPause,
|
onPlayerPause,
|
||||||
onPlayerPlay
|
onPlayerPlay,
|
||||||
|
onPlayerUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,44 +12,80 @@ module.exports = {
|
|||||||
* or activating the window.
|
* or activating the window.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var config = require('../config')
|
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.
|
* Show the Windows thumbnail toolbar buttons.
|
||||||
*/
|
*/
|
||||||
function enable () {
|
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.
|
* Hide the Windows thumbnail toolbar buttons.
|
||||||
*/
|
*/
|
||||||
function disable () {
|
function disable () {
|
||||||
windows.main.win.setThumbarButtons([])
|
buttons = []
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerPause () {
|
function onPlayerPause () {
|
||||||
update(true)
|
if (!isEnabled()) return
|
||||||
|
buttons[PLAY_PAUSE].tooltip = 'Play'
|
||||||
|
buttons[PLAY_PAUSE].icon = PLAY_ICON
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerPlay () {
|
function onPlayerPlay () {
|
||||||
update(false)
|
if (!isEnabled()) return
|
||||||
|
buttons[PLAY_PAUSE].tooltip = 'Pause'
|
||||||
|
buttons[PLAY_PAUSE].icon = PAUSE_ICON
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function update (isPaused) {
|
function onPlayerUpdate (state) {
|
||||||
var icon = isPaused
|
if (!isEnabled()) return
|
||||||
? 'PlayThumbnailBarButton.png'
|
buttons[PREV].flags = [ state.hasPrevious ? 'enabled' : 'disabled' ]
|
||||||
: 'PauseThumbnailBarButton.png'
|
buttons[NEXT].flags = [ state.hasNext ? 'enabled' : 'disabled' ]
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
var buttons = [
|
function isEnabled () {
|
||||||
{
|
return buttons.length > 0
|
||||||
tooltip: isPaused ? 'Play' : 'Pause',
|
}
|
||||||
icon: path.join(config.STATIC_PATH, icon),
|
|
||||||
click: () => windows.main.dispatch('playPause')
|
function update () {
|
||||||
}
|
|
||||||
]
|
|
||||||
windows.main.win.setThumbarButtons(buttons)
|
windows.main.win.setThumbarButtons(buttons)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
hasTray,
|
hasTray,
|
||||||
init,
|
init,
|
||||||
onWindowBlur,
|
setWindowFocus
|
||||||
onWindowFocus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
var tray
|
let tray
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
@@ -31,12 +30,7 @@ function hasTray () {
|
|||||||
return !!tray
|
return !!tray
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
function setWindowFocus (flag) {
|
||||||
if (!tray) return
|
|
||||||
updateTrayMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowFocus () {
|
|
||||||
if (!tray) return
|
if (!tray) return
|
||||||
updateTrayMenu()
|
updateTrayMenu()
|
||||||
}
|
}
|
||||||
@@ -55,7 +49,7 @@ function initWin32 () {
|
|||||||
* Check for libappindicator1 support before creating tray icon
|
* Check for libappindicator1 support before creating tray icon
|
||||||
*/
|
*/
|
||||||
function checkLinuxTraySupport (cb) {
|
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
|
// 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
|
// libappindicator1. If WebTorrent was installed from the deb file, we should
|
||||||
@@ -80,7 +74,7 @@ function createTray () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateTrayMenu () {
|
function updateTrayMenu () {
|
||||||
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||||
tray.setContextMenu(contextMenu)
|
tray.setContextMenu(contextMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ module.exports = {
|
|||||||
init
|
init
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var get = require('simple-get')
|
const get = require('simple-get')
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||||
'?version=' + config.APP_VERSION +
|
'?version=' + config.APP_VERSION +
|
||||||
'&platform=' + process.platform
|
'&platform=' + process.platform
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ module.exports = {
|
|||||||
init
|
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)
|
* Add a user task menu to the app icon on right-click. (Windows)
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
checkForVLC,
|
|
||||||
spawn
|
|
||||||
}
|
|
||||||
|
|
||||||
var cp = require('child_process')
|
|
||||||
var vlcCommand = require('vlc-command')
|
|
||||||
|
|
||||||
// Finds if VLC is installed on Mac, Windows, or Linux.
|
|
||||||
// Calls back with true or false: whether VLC was detected
|
|
||||||
function checkForVLC (cb) {
|
|
||||||
vlcCommand((err) => cb(!err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawns VLC with child_process.spawn() to return a ChildProcess object
|
|
||||||
// Calls back with (err, childProcess)
|
|
||||||
function spawn (args, cb) {
|
|
||||||
vlcCommand(function (err, vlcPath) {
|
|
||||||
if (err) return cb(err)
|
|
||||||
cb(null, cp.spawn(vlcPath, args))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
var about = module.exports = {
|
const about = module.exports = {
|
||||||
init,
|
init,
|
||||||
win: null
|
win: null
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
if (about.win) {
|
if (about.win) {
|
||||||
return about.win.show()
|
return about.win.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
var win = about.win = new electron.BrowserWindow({
|
const win = about.win = new electron.BrowserWindow({
|
||||||
backgroundColor: '#ECECEC',
|
backgroundColor: '#ECECEC',
|
||||||
center: true,
|
center: true,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
var main = module.exports = {
|
const main = module.exports = {
|
||||||
dispatch,
|
dispatch,
|
||||||
hide,
|
hide,
|
||||||
init,
|
init,
|
||||||
@@ -14,24 +14,24 @@ var main = module.exports = {
|
|||||||
win: null
|
win: null
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var log = require('../log')
|
const log = require('../log')
|
||||||
var menu = require('../menu')
|
const menu = require('../menu')
|
||||||
var tray = require('../tray')
|
const tray = require('../tray')
|
||||||
|
|
||||||
var HEADER_HEIGHT = 37
|
const HEADER_HEIGHT = 38
|
||||||
var TORRENT_HEIGHT = 100
|
const TORRENT_HEIGHT = 100
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
if (main.win) {
|
if (main.win) {
|
||||||
return main.win.show()
|
return main.win.show()
|
||||||
}
|
}
|
||||||
var win = main.win = new electron.BrowserWindow({
|
const win = main.win = new electron.BrowserWindow({
|
||||||
backgroundColor: '#1E1E1E',
|
backgroundColor: '#282828',
|
||||||
darkTheme: true, // Forces dark theme (GTK+3)
|
darkTheme: true, // Forces dark theme (GTK+3)
|
||||||
icon: getIconPath(), // Window icon (Windows, Linux)
|
icon: getIconPath(), // Window icon (Windows, Linux)
|
||||||
minWidth: config.WINDOW_MIN_WIDTH,
|
minWidth: config.WINDOW_MIN_WIDTH,
|
||||||
@@ -114,7 +114,7 @@ function setBounds (bounds, maximize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Maximize or minimize, if the second argument is present
|
// Maximize or minimize, if the second argument is present
|
||||||
var willBeMaximized
|
let willBeMaximized
|
||||||
if (maximize === true) {
|
if (maximize === true) {
|
||||||
if (!main.win.isMaximized()) {
|
if (!main.win.isMaximized()) {
|
||||||
log('setBounds: maximizing')
|
log('setBounds: maximizing')
|
||||||
@@ -136,12 +136,18 @@ function setBounds (bounds, maximize) {
|
|||||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
||||||
if (bounds.x === null && bounds.y === null) {
|
if (bounds.x === null && bounds.y === null) {
|
||||||
// X and Y not specified? By default, center on current screen
|
// X and Y not specified? By default, center on current screen
|
||||||
var scr = electron.screen.getDisplayMatching(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.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
|
||||||
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
|
||||||
log('setBounds: centered to ' + JSON.stringify(bounds))
|
log('setBounds: centered to ' + JSON.stringify(bounds))
|
||||||
}
|
}
|
||||||
main.win.setBounds(bounds, true)
|
// Resize the window's content area (so window border doesn't need to be taken
|
||||||
|
// into account)
|
||||||
|
if (bounds.contentBounds) {
|
||||||
|
main.win.setContentBounds(bounds, true)
|
||||||
|
} else {
|
||||||
|
main.win.setBounds(bounds, true)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log('setBounds: not setting bounds because of window maximization')
|
log('setBounds: not setting bounds because of window maximization')
|
||||||
}
|
}
|
||||||
@@ -204,13 +210,13 @@ function toggleFullScreen (flag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onWindowBlur () {
|
function onWindowBlur () {
|
||||||
menu.onWindowBlur()
|
menu.setWindowFocus(false)
|
||||||
tray.onWindowBlur()
|
tray.setWindowFocus(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowFocus () {
|
function onWindowFocus () {
|
||||||
menu.onWindowFocus()
|
menu.setWindowFocus(true)
|
||||||
tray.onWindowFocus()
|
tray.setWindowFocus(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconPath () {
|
function getIconPath () {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
var webtorrent = module.exports = {
|
const webtorrent = module.exports = {
|
||||||
init,
|
init,
|
||||||
send,
|
send,
|
||||||
show,
|
show,
|
||||||
@@ -6,13 +6,13 @@ var webtorrent = module.exports = {
|
|||||||
win: null
|
win: null
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var log = require('../log')
|
const log = require('../log')
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
var win = webtorrent.win = new electron.BrowserWindow({
|
const win = webtorrent.win = new electron.BrowserWindow({
|
||||||
backgroundColor: '#1E1E1E',
|
backgroundColor: '#1E1E1E',
|
||||||
center: true,
|
center: true,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module.exports = class CreateTorrentErrorPage extends React.Component {
|
|||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
<p className='float-right'>
|
<p className='float-right'>
|
||||||
<button className='button-flat light' onClick={dispatcher('back')}>
|
<button className='button-flat light' onClick={dispatcher('cancel')}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
@@ -2,11 +2,11 @@ const React = require('react')
|
|||||||
|
|
||||||
const {dispatcher} = require('../lib/dispatcher')
|
const {dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class Header extends React.Component {
|
class Header extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
var loc = this.props.state.location
|
const loc = this.props.state.location
|
||||||
return (
|
return (
|
||||||
<div key='header' className='header'>
|
<div className='header'>
|
||||||
{this.getTitle()}
|
{this.getTitle()}
|
||||||
<div className='nav left float-left'>
|
<div className='nav left float-left'>
|
||||||
<i
|
<i
|
||||||
@@ -31,12 +31,12 @@ module.exports = class Header extends React.Component {
|
|||||||
|
|
||||||
getTitle () {
|
getTitle () {
|
||||||
if (process.platform !== 'darwin') return null
|
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>)
|
return (<div className='title ellipsis'>{state.window.title}</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddButton () {
|
getAddButton () {
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
if (state.location.url() !== 'home') return null
|
if (state.location.url() !== 'home') return null
|
||||||
return (
|
return (
|
||||||
<i
|
<i
|
||||||
@@ -48,3 +48,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'
|
||||||
|
style={cancelStyle}
|
||||||
|
label={cancelText}
|
||||||
|
onClick={onCancel} />
|
||||||
|
<RaisedButton
|
||||||
|
className='control'
|
||||||
|
primary
|
||||||
|
label={okText}
|
||||||
|
onClick={onOK} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/renderer/components/open-torrent-address-modal.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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
|
||||||
|
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) this.handleOK() /* 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 React = require('react')
|
||||||
|
|
||||||
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class RemoveTorrentModal extends React.Component {
|
module.exports = class RemoveTorrentModal extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
var message = state.modal.deleteData
|
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 and delete the data file?'
|
||||||
: 'Are you sure you want to remove this torrent from the list?'
|
: '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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p><strong>{message}</strong></p>
|
<p><strong>{message}</strong></p>
|
||||||
<p className='float-right'>
|
<ModalOKCancel
|
||||||
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
|
cancelText='CANCEL'
|
||||||
<button className='button button-raised' onClick={handleRemove}>{buttonText}</button>
|
onCancel={dispatcher('exitModal')}
|
||||||
</p>
|
okText={buttonText}
|
||||||
|
onOK={handleRemove} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
52
src/renderer/components/show-more.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const React = require('react')
|
||||||
|
|
||||||
|
const FlatButton = require('material-ui/FlatButton').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 style={this.props.style}>
|
||||||
|
{this.state.expanded ? this.props.children : null}
|
||||||
|
<FlatButton
|
||||||
|
onClick={this.handleClick}
|
||||||
|
label={label} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ShowMore
|
||||||
49
src/renderer/components/unsupported-media-modal.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const React = require('react')
|
||||||
|
const electron = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
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 playerPath = state.saved.prefs.externalPlayerPath
|
||||||
|
const playerName = playerPath
|
||||||
|
? path.basename(playerPath).split('.')[0]
|
||||||
|
: 'VLC'
|
||||||
|
const onAction = state.modal.externalPlayerInstalled
|
||||||
|
? dispatcher('openExternalPlayer')
|
||||||
|
: () => this.onInstall()
|
||||||
|
const actionText = state.modal.externalPlayerInstalled
|
||||||
|
? 'PLAY IN ' + playerName.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,28 +1,33 @@
|
|||||||
const React = require('react')
|
const React = require('react')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class UpdateAvailableModal extends React.Component {
|
module.exports = class UpdateAvailableModal extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
return (
|
return (
|
||||||
<div className='update-available-modal'>
|
<div className='update-available-modal'>
|
||||||
<p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p>
|
<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>
|
||||||
<p className='float-right'>
|
We have an auto-updater for Windows and Mac.
|
||||||
<button className='button button-flat' onClick={handleCancel}>Skip This Release</button>
|
We don't have one for Linux yet, so you'll have to download the new version manually.
|
||||||
<button className='button button-raised' onClick={handleOK}>Show Download Page</button>
|
|
||||||
</p>
|
</p>
|
||||||
|
<ModalOKCancel
|
||||||
|
cancelText='SKIP THIS RELEASE'
|
||||||
|
onCancel={handleSkip}
|
||||||
|
okText='SHOW DOWNLOAD PAGE'
|
||||||
|
onOK={handleShow} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
function handleOK () {
|
function handleShow () {
|
||||||
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
|
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
|
||||||
dispatch('exitModal')
|
dispatch('exitModal')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel () {
|
function handleSkip () {
|
||||||
dispatch('skipVersion', state.modal.version)
|
dispatch('skipVersion', state.modal.version)
|
||||||
dispatch('exitModal')
|
dispatch('exitModal')
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ const electron = require('electron')
|
|||||||
|
|
||||||
const ipcRenderer = electron.ipcRenderer
|
const ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
|
const Playlist = require('../lib/playlist')
|
||||||
|
|
||||||
// Controls local play back: the <video>/<audio> tag and VLC
|
// Controls local play back: the <video>/<audio> tag and VLC
|
||||||
// Does not control remote casting (Chromecast etc)
|
// Does not control remote casting (Chromecast etc)
|
||||||
module.exports = class MediaController {
|
module.exports = class MediaController {
|
||||||
@@ -18,16 +20,16 @@ module.exports = class MediaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mediaError (error) {
|
mediaError (error) {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (state.location.url() === 'player') {
|
if (state.location.url() === 'player') {
|
||||||
state.playing.result = 'error'
|
state.playing.result = 'error'
|
||||||
state.playing.location = 'error'
|
state.playing.location = 'error'
|
||||||
ipcRenderer.send('checkForVLC')
|
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
|
||||||
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
|
||||||
state.modal = {
|
state.modal = {
|
||||||
id: 'unsupported-media-modal',
|
id: 'unsupported-media-modal',
|
||||||
error: error,
|
error: error,
|
||||||
vlcInstalled: isInstalled
|
externalPlayerInstalled: isInstalled
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -42,15 +44,20 @@ module.exports = class MediaController {
|
|||||||
this.state.playing.mouseStationarySince = new Date().getTime()
|
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
vlcPlay () {
|
openExternalPlayer () {
|
||||||
ipcRenderer.send('vlcPlay', this.state.server.localURL, this.state.window.title)
|
const state = this.state
|
||||||
this.state.playing.location = 'vlc'
|
const mediaURL = Playlist.getCurrentLocalURL(this.state)
|
||||||
|
ipcRenderer.send('openExternalPlayer',
|
||||||
|
state.saved.prefs.externalPlayerPath,
|
||||||
|
mediaURL,
|
||||||
|
state.window.title)
|
||||||
|
state.playing.location = 'external'
|
||||||
}
|
}
|
||||||
|
|
||||||
vlcNotFound () {
|
externalPlayerNotFound () {
|
||||||
var modal = this.state.modal
|
const modal = this.state.modal
|
||||||
if (modal && modal.id === 'unsupported-media-modal') {
|
if (modal && modal.id === 'unsupported-media-modal') {
|
||||||
modal.vlcNotFound = true
|
modal.externalPlayerNotFound = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ const errors = require('../lib/errors')
|
|||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const Playlist = require('../lib/playlist')
|
||||||
const State = require('../lib/state')
|
const State = require('../lib/state')
|
||||||
|
|
||||||
const ipcRenderer = electron.ipcRenderer
|
const ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
// Controls playback of torrents and files within torrents
|
// Controls playback of torrents and files within torrents
|
||||||
// both local (<video>,<audio>,VLC) and remote (cast)
|
// both local (<video>,<audio>,external player) and remote (cast)
|
||||||
module.exports = class PlaybackController {
|
module.exports = class PlaybackController {
|
||||||
constructor (state, config, update) {
|
constructor (state, config, update) {
|
||||||
this.state = state
|
this.state = state
|
||||||
@@ -24,24 +25,43 @@ module.exports = class PlaybackController {
|
|||||||
// Play a file in a torrent.
|
// Play a file in a torrent.
|
||||||
// * Start torrenting, if necessary
|
// * Start torrenting, if necessary
|
||||||
// * Stream, if not already fully downloaded
|
// * 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 */) {
|
playFile (infoHash, index /* optional */) {
|
||||||
this.state.location.go({
|
const state = this.state
|
||||||
url: 'player',
|
if (state.location.url() === 'player') {
|
||||||
setup: (cb) => {
|
this.updatePlayer(infoHash, index, false, (err) => {
|
||||||
this.play()
|
if (err) dispatch('error', err)
|
||||||
this.openPlayer(infoHash, index, cb)
|
else this.play()
|
||||||
},
|
})
|
||||||
destroy: () => this.closePlayer()
|
} else {
|
||||||
}, (err) => {
|
let initialized = false
|
||||||
if (err) dispatch('error', err)
|
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 errors.UnplayableError())
|
||||||
|
|
||||||
|
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) {
|
openItem (infoHash, index) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
var filePath = path.join(
|
const filePath = path.join(
|
||||||
torrentSummary.path,
|
torrentSummary.path,
|
||||||
torrentSummary.files[index].path)
|
torrentSummary.files[index].path)
|
||||||
ipcRenderer.send('openItem', filePath)
|
ipcRenderer.send('openItem', filePath)
|
||||||
@@ -49,12 +69,12 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Toggle (play or pause) the currently playing media
|
// Toggle (play or pause) the currently playing media
|
||||||
playPause () {
|
playPause () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (state.location.url() !== 'player') return
|
if (state.location.url() !== 'player') return
|
||||||
|
|
||||||
// force rerendering if window is hidden,
|
// force rerendering if window is hidden,
|
||||||
// in order to bypass `raf` and play/pause media immediately
|
// 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.window.isVisible && mediaTag) {
|
||||||
if (state.playing.isPaused) mediaTag.play()
|
if (state.playing.isPaused) mediaTag.play()
|
||||||
else mediaTag.pause()
|
else mediaTag.pause()
|
||||||
@@ -64,9 +84,33 @@ module.exports = class PlaybackController {
|
|||||||
else this.pause()
|
else this.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Play next file in list (if any)
|
||||||
|
nextTrack () {
|
||||||
|
const state = this.state
|
||||||
|
if (Playlist.hasNext(state)) {
|
||||||
|
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)) {
|
||||||
|
this.updatePlayer(
|
||||||
|
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
||||||
|
if (err) dispatch('error', err)
|
||||||
|
else this.play()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Play (unpause) the current media
|
// Play (unpause) the current media
|
||||||
play () {
|
play () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (!state.playing.isPaused) return
|
if (!state.playing.isPaused) return
|
||||||
state.playing.isPaused = false
|
state.playing.isPaused = false
|
||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
@@ -77,7 +121,7 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Pause the currently playing media
|
// Pause the currently playing media
|
||||||
pause () {
|
pause () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (state.playing.isPaused) return
|
if (state.playing.isPaused) return
|
||||||
state.playing.isPaused = true
|
state.playing.isPaused = true
|
||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
@@ -93,6 +137,10 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Skip (aka seek) to a specific point, in seconds
|
// Skip (aka seek) to a specific point, in seconds
|
||||||
skipTo (time) {
|
skipTo (time) {
|
||||||
|
if (!Number.isFinite(time)) {
|
||||||
|
console.error('Tried to skip to a non-finite time ' + time)
|
||||||
|
return console.trace()
|
||||||
|
}
|
||||||
if (isCasting(this.state)) Cast.seek(time)
|
if (isCasting(this.state)) Cast.seek(time)
|
||||||
else this.state.playing.jumpToTime = time
|
else this.state.playing.jumpToTime = time
|
||||||
}
|
}
|
||||||
@@ -102,19 +150,22 @@ module.exports = class PlaybackController {
|
|||||||
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
|
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
|
||||||
// until -16 (fast rewind)
|
// until -16 (fast rewind)
|
||||||
changePlaybackRate (direction) {
|
changePlaybackRate (direction) {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
var rate = state.playing.playbackRate
|
let rate = state.playing.playbackRate
|
||||||
if (direction > 0 && rate >= 0.25 && rate < 2) {
|
if (direction > 0 && rate >= 0.25 && rate < 2) {
|
||||||
rate += 0.25
|
rate += 0.25
|
||||||
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
||||||
rate -= 0.25
|
rate -= 0.25
|
||||||
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
|
} else if (direction < 0 && rate === 0.25) {
|
||||||
|
// When we set playback rate at 0 in html 5, playback hangs ;(
|
||||||
rate = -1
|
rate = -1
|
||||||
} else if (direction > 0 && rate === -1) {
|
} else if (direction > 0 && rate === -1) {
|
||||||
rate = 0.25
|
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) ||
|
||||||
|
(direction < 0 && rate > -16 && rate <= -1)) {
|
||||||
rate *= 2
|
rate *= 2
|
||||||
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
|
} else if ((direction < 0 && rate > 1 && rate <= 16) ||
|
||||||
|
(direction > 0 && rate >= -16 && rate < -1)) {
|
||||||
rate /= 2
|
rate /= 2
|
||||||
}
|
}
|
||||||
state.playing.playbackRate = rate
|
state.playing.playbackRate = rate
|
||||||
@@ -135,7 +186,7 @@ module.exports = class PlaybackController {
|
|||||||
// check if its in [0.0 - 1.0] range
|
// check if its in [0.0 - 1.0] range
|
||||||
volume = Math.max(0, Math.min(1, volume))
|
volume = Math.max(0, Math.min(1, volume))
|
||||||
|
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
Cast.setVolume(volume)
|
Cast.setVolume(volume)
|
||||||
} else {
|
} else {
|
||||||
@@ -149,8 +200,8 @@ module.exports = class PlaybackController {
|
|||||||
// * The video is paused
|
// * The video is paused
|
||||||
// * The video is playing remotely on Chromecast or Airplay
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
showOrHidePlayerControls () {
|
showOrHidePlayerControls () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
var hideControls = state.location.url() === 'player' &&
|
const hideControls = state.location.url() === 'player' &&
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
state.playing.mouseStationarySince !== 0 &&
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||||
!state.playing.isPaused &&
|
!state.playing.isPaused &&
|
||||||
@@ -165,12 +216,10 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Opens the video player to a specific torrent
|
// Opens the video player to a specific torrent
|
||||||
openPlayer (infoHash, index, cb) {
|
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
|
state.playing.infoHash = torrentSummary.infoHash
|
||||||
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
|
|
||||||
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
|
|
||||||
if (index === undefined) return cb(new errors.UnplayableError())
|
|
||||||
|
|
||||||
// update UI to show pending playback
|
// update UI to show pending playback
|
||||||
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
||||||
@@ -178,7 +227,7 @@ module.exports = class PlaybackController {
|
|||||||
torrentSummary.playStatus = 'requested'
|
torrentSummary.playStatus = 'requested'
|
||||||
this.update()
|
this.update()
|
||||||
|
|
||||||
var timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
telemetry.logPlayAttempt('timeout')
|
telemetry.logPlayAttempt('timeout')
|
||||||
// TODO: remove torrentSummary.playStatus
|
// TODO: remove torrentSummary.playStatus
|
||||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||||
@@ -187,34 +236,69 @@ module.exports = class PlaybackController {
|
|||||||
this.update()
|
this.update()
|
||||||
}, 10000) /* give it a few seconds */
|
}, 10000) /* give it a few seconds */
|
||||||
|
|
||||||
|
this.startServer(torrentSummary, () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
// if we timed out (user clicked play a long time ago), don't autoplay
|
||||||
|
const timedOut = torrentSummary.playStatus === 'timeout'
|
||||||
|
delete torrentSummary.playStatus
|
||||||
|
if (timedOut) {
|
||||||
|
ipcRenderer.send('wt-stop-server')
|
||||||
|
return this.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.send('onPlayerOpen')
|
||||||
|
this.updatePlayer(infoHash, index, true, cb)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts WebTorrent server for media streaming
|
||||||
|
startServer (torrentSummary, cb) {
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
dispatch('startTorrentingSummary', torrentSummary)
|
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
||||||
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||||
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
() => onTorrentReady())
|
||||||
} else {
|
} else {
|
||||||
this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
|
onTorrentReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTorrentReady () {
|
||||||
|
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
|
||||||
|
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, () => cb())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
// Called each time the current file changes
|
||||||
var fileSummary = torrentSummary.files[index]
|
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 Error('Can\'t play that file'))
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSummary.mostRecentFileIndex = index
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
var state = this.state
|
state.playing.infoHash = infoHash
|
||||||
state.playing.infoHash = torrentSummary.infoHash
|
|
||||||
state.playing.fileIndex = index
|
state.playing.fileIndex = index
|
||||||
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
||||||
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||||
: 'other'
|
: 'other'
|
||||||
|
|
||||||
// pick up where we left off
|
// pick up where we left off
|
||||||
if (fileSummary.currentTime) {
|
let jumpToTime = 0
|
||||||
var fraction = fileSummary.currentTime / fileSummary.duration
|
if (resume && fileSummary.currentTime) {
|
||||||
var secondsLeft = fileSummary.duration - fileSummary.currentTime
|
const fraction = fileSummary.currentTime / fileSummary.duration
|
||||||
|
const secondsLeft = fileSummary.duration - fileSummary.currentTime
|
||||||
if (fraction < 0.9 && secondsLeft > 10) {
|
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 it's audio, parse out the metadata (artist, title, etc)
|
||||||
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
||||||
@@ -229,48 +313,43 @@ module.exports = class PlaybackController {
|
|||||||
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
state.window.title = fileSummary.name
|
||||||
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
|
|
||||||
// if we timed out (user clicked play a long time ago), don't autoplay
|
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
||||||
var timedOut = torrentSummary.playStatus === 'timeout'
|
if (this.state.saved.prefs.openExternalPlayer) {
|
||||||
delete torrentSummary.playStatus
|
dispatch('openExternalPlayer')
|
||||||
if (timedOut) {
|
|
||||||
ipcRenderer.send('wt-stop-server')
|
|
||||||
return this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, play the video
|
|
||||||
dispatch('setTitle', torrentSummary.files[state.playing.fileIndex].name)
|
|
||||||
this.update()
|
this.update()
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerOpen')
|
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, play the video
|
||||||
|
this.update()
|
||||||
|
|
||||||
|
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
|
||||||
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
closePlayer () {
|
closePlayer () {
|
||||||
console.log('closePlayer')
|
console.log('closePlayer')
|
||||||
|
|
||||||
// Quit any external players, like Chromecast/Airplay/etc or VLC
|
// Quit any external players, like Chromecast/Airplay/etc or VLC
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
Cast.stop()
|
Cast.stop()
|
||||||
}
|
}
|
||||||
if (state.playing.location === 'vlc') {
|
if (state.playing.location === 'external') {
|
||||||
ipcRenderer.send('vlcQuit')
|
ipcRenderer.send('quitExternalPlayer')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save volume (this session only, not in state.saved)
|
// Save volume (this session only, not in state.saved)
|
||||||
state.previousVolume = state.playing.volume
|
state.previousVolume = state.playing.volume
|
||||||
|
|
||||||
// Telemetry: track what happens after the user clicks play
|
// 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
|
if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed
|
||||||
else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc
|
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)
|
else console.error('Unknown state.playing.result', state.playing.result)
|
||||||
|
|
||||||
// Reset the window contents back to the home screen
|
// Reset the window contents back to the home screen
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const {dispatch} = require('../lib/dispatcher')
|
|
||||||
const State = require('../lib/state')
|
const State = require('../lib/state')
|
||||||
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
// Controls the Preferences screen
|
// Controls the Preferences screen
|
||||||
module.exports = class PrefsController {
|
module.exports = class PrefsController {
|
||||||
@@ -10,16 +11,20 @@ module.exports = class PrefsController {
|
|||||||
|
|
||||||
// Goes to the Preferences screen
|
// Goes to the Preferences screen
|
||||||
show () {
|
show () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
state.location.go({
|
state.location.go({
|
||||||
url: 'preferences',
|
url: 'preferences',
|
||||||
setup: function (cb) {
|
setup: function (cb) {
|
||||||
// initialize preferences
|
// initialize preferences
|
||||||
dispatch('setTitle', 'Preferences')
|
state.window.title = 'Preferences'
|
||||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||||
|
ipcRenderer.send('setAllowNav', false)
|
||||||
cb()
|
cb()
|
||||||
},
|
},
|
||||||
destroy: () => this.save()
|
destroy: () => {
|
||||||
|
ipcRenderer.send('setAllowNav', true)
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,21 +32,26 @@ module.exports = class PrefsController {
|
|||||||
// For example: updatePreferences('foo.bar', 'baz')
|
// For example: updatePreferences('foo.bar', 'baz')
|
||||||
// Call save() to save to config.json
|
// Call save() to save to config.json
|
||||||
update (property, value) {
|
update (property, value) {
|
||||||
var path = property.split('.')
|
const path = property.split('.')
|
||||||
var key = this.state.unsaved.prefs
|
let obj = this.state.unsaved.prefs
|
||||||
for (var i = 0; i < path.length - 1; i++) {
|
let i
|
||||||
if (typeof key[path[i]] === 'undefined') {
|
for (i = 0; i < path.length - 1; i++) {
|
||||||
key[path[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
|
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||||
save () {
|
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)
|
||||||
|
}
|
||||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||||
State.save(state)
|
State.save(state)
|
||||||
|
dispatch('checkDownloadPath')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ const fs = require('fs-extra')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const parallel = require('run-parallel')
|
const parallel = require('run-parallel')
|
||||||
|
|
||||||
|
const remote = electron.remote
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class SubtitlesController {
|
module.exports = class SubtitlesController {
|
||||||
@@ -11,7 +13,7 @@ module.exports = class SubtitlesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openSubtitles () {
|
openSubtitles () {
|
||||||
electron.remote.dialog.showOpenDialog({
|
remote.dialog.showOpenDialog({
|
||||||
title: 'Select a subtitles file.',
|
title: 'Select a subtitles file.',
|
||||||
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
||||||
properties: [ 'openFile' ]
|
properties: [ 'openFile' ]
|
||||||
@@ -26,26 +28,26 @@ module.exports = class SubtitlesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleSubtitlesMenu () {
|
toggleSubtitlesMenu () {
|
||||||
var subtitles = this.state.playing.subtitles
|
const subtitles = this.state.playing.subtitles
|
||||||
subtitles.showMenu = !subtitles.showMenu
|
subtitles.showMenu = !subtitles.showMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
addSubtitles (files, autoSelect) {
|
addSubtitles (files, autoSelect) {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
// Subtitles are only supported when playing video files
|
// Subtitles are only supported when playing video files
|
||||||
if (state.playing.type !== 'video') return
|
if (state.playing.type !== 'video') return
|
||||||
if (files.length === 0) return
|
if (files.length === 0) return
|
||||||
var subtitles = state.playing.subtitles
|
const subtitles = state.playing.subtitles
|
||||||
|
|
||||||
// Read the files concurrently, then add all resulting subtitle tracks
|
// 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) {
|
parallel(tasks, function (err, tracks) {
|
||||||
if (err) return dispatch('error', err)
|
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
|
// No dupes allowed
|
||||||
var track = tracks[i]
|
const track = tracks[i]
|
||||||
var trackIndex = state.playing.subtitles.tracks
|
let trackIndex = state.playing.subtitles.tracks
|
||||||
.findIndex((t) => track.filePath === t.filePath)
|
.findIndex((t) => track.filePath === t.filePath)
|
||||||
|
|
||||||
// Add the track
|
// Add the track
|
||||||
@@ -66,46 +68,46 @@ module.exports = class SubtitlesController {
|
|||||||
|
|
||||||
checkForSubtitles () {
|
checkForSubtitles () {
|
||||||
if (this.state.playing.type !== 'video') return
|
if (this.state.playing.type !== 'video') return
|
||||||
var torrentSummary = this.state.getPlayingTorrentSummary()
|
const torrentSummary = this.state.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary || !torrentSummary.progress) return
|
if (!torrentSummary || !torrentSummary.progress) return
|
||||||
|
|
||||||
torrentSummary.progress.files.forEach((fp, ix) => {
|
torrentSummary.progress.files.forEach((fp, ix) => {
|
||||||
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
|
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
|
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)
|
this.addSubtitles([filePath], false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isSubtitle (file) {
|
isSubtitle (file) {
|
||||||
var name = typeof file === 'string' ? file : file.name
|
const name = typeof file === 'string' ? file : file.name
|
||||||
var ext = path.extname(name).toLowerCase()
|
const ext = path.extname(name).toLowerCase()
|
||||||
return ext === '.srt' || ext === '.vtt'
|
return ext === '.srt' || ext === '.vtt'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSubtitle (file, cb) {
|
function loadSubtitle (file, cb) {
|
||||||
// Lazy load to keep startup fast
|
// Lazy load to keep startup fast
|
||||||
var concat = require('simple-concat')
|
const concat = require('simple-concat')
|
||||||
var LanguageDetect = require('languagedetect')
|
const LanguageDetect = require('languagedetect')
|
||||||
var srtToVtt = require('srt-to-vtt')
|
const srtToVtt = require('srt-to-vtt')
|
||||||
|
|
||||||
// Read the .SRT or .VTT file, parse it, add subtitle track
|
// 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) {
|
concat(vttStream, function (err, buf) {
|
||||||
if (err) return dispatch('error', 'Can\'t parse subtitles file.')
|
if (err) return dispatch('error', 'Can\'t parse subtitles file.')
|
||||||
|
|
||||||
// Detect what language the subtitles are in
|
// Detect what language the subtitles are in
|
||||||
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
const vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
||||||
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
let langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
||||||
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
||||||
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
||||||
|
|
||||||
var track = {
|
const track = {
|
||||||
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
||||||
language: langDetected,
|
language: langDetected,
|
||||||
label: langDetected,
|
label: langDetected,
|
||||||
@@ -119,18 +121,18 @@ function loadSubtitle (file, cb) {
|
|||||||
// Checks whether a language name like 'English' or 'German' matches the system
|
// Checks whether a language name like 'English' or 'German' matches the system
|
||||||
// language, aka the current locale
|
// language, aka the current locale
|
||||||
function isSystemLanguage (language) {
|
function isSystemLanguage (language) {
|
||||||
var iso639 = require('iso-639-1')
|
const iso639 = require('iso-639-1')
|
||||||
var osLangISO = window.navigator.language.split('-')[0] // eg 'en'
|
const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
|
||||||
var langIso = iso639.getCode(language) // eg 'de' if language is 'German'
|
const langIso = iso639.getCode(language) // eg 'de' if language is 'German'
|
||||||
return langIso === osLangISO
|
return langIso === osLangISO
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't have two subtitle tracks with the same label
|
// Make sure we don't have two subtitle tracks with the same label
|
||||||
// Labels each track by language, eg 'German', 'English', 'English 2', ...
|
// Labels each track by language, eg 'German', 'English', 'English 2', ...
|
||||||
function relabelSubtitles (subtitles) {
|
function relabelSubtitles (subtitles) {
|
||||||
var counts = {}
|
const counts = {}
|
||||||
subtitles.tracks.forEach(function (track) {
|
subtitles.tracks.forEach(function (track) {
|
||||||
var lang = track.language
|
const lang = track.language
|
||||||
counts[lang] = (counts[lang] || 0) + 1
|
counts[lang] = (counts[lang] || 0) + 1
|
||||||
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
|
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ const path = require('path')
|
|||||||
const ipcRenderer = require('electron').ipcRenderer
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
|
||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
@@ -12,12 +11,12 @@ module.exports = class TorrentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
torrentInfoHash (torrentKey, infoHash) {
|
torrentInfoHash (torrentKey, infoHash) {
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
let torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
console.log('got infohash for %s torrent %s',
|
console.log('got infohash for %s torrent %s',
|
||||||
torrentSummary ? 'existing' : 'new', torrentKey)
|
torrentSummary ? 'existing' : 'new', torrentKey)
|
||||||
|
|
||||||
if (!torrentSummary) {
|
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
|
// Check if an existing (non-active) torrent has the same info hash
|
||||||
if (torrents.find((t) => t.infoHash === infoHash)) {
|
if (torrents.find((t) => t.infoHash === infoHash)) {
|
||||||
@@ -49,7 +48,7 @@ module.exports = class TorrentController {
|
|||||||
}
|
}
|
||||||
dispatch('error', message)
|
dispatch('error', message)
|
||||||
|
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
if (torrentSummary) {
|
if (torrentSummary) {
|
||||||
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
|
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
|
||||||
torrentSummary.status = 'paused'
|
torrentSummary.status = 'paused'
|
||||||
@@ -59,21 +58,20 @@ module.exports = class TorrentController {
|
|||||||
|
|
||||||
torrentMetadata (torrentKey, torrentInfo) {
|
torrentMetadata (torrentKey, torrentInfo) {
|
||||||
// Summarize torrent
|
// Summarize torrent
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.status = 'downloading'
|
torrentSummary.status = 'downloading'
|
||||||
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
||||||
torrentSummary.path = torrentInfo.path
|
torrentSummary.path = torrentInfo.path
|
||||||
torrentSummary.magnetURI = torrentInfo.magnetURI
|
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||||
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
||||||
// For now, check whether torrentSummary.files has already been set:
|
// 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) {
|
if (!hasDetailedFileInfo) {
|
||||||
torrentSummary.files = torrentInfo.files
|
torrentSummary.files = torrentInfo.files
|
||||||
}
|
}
|
||||||
if (!torrentSummary.selections) {
|
if (!torrentSummary.selections) {
|
||||||
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||||
}
|
}
|
||||||
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
|
|
||||||
dispatch('update')
|
dispatch('update')
|
||||||
|
|
||||||
// Save the .torrent file, if it hasn't been saved already
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
@@ -85,7 +83,7 @@ module.exports = class TorrentController {
|
|||||||
|
|
||||||
torrentDone (torrentKey, torrentInfo) {
|
torrentDone (torrentKey, torrentInfo) {
|
||||||
// Update the torrent summary
|
// Update the torrent summary
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.status = 'seeding'
|
torrentSummary.status = 'seeding'
|
||||||
|
|
||||||
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
|
// 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) {
|
torrentProgress (progressInfo) {
|
||||||
// Overall progress across all active torrents, 0 to 1
|
// Overall progress across all active torrents, 0 to 1, or -1 to hide the progress bar
|
||||||
var progress = progressInfo.progress
|
|
||||||
var hasActiveTorrents = progressInfo.hasActiveTorrents
|
|
||||||
|
|
||||||
// Hide progress bar when client has no torrents, or progress is 100%
|
// Hide progress bar when client has no torrents, or progress is 100%
|
||||||
// TODO: isn't this equivalent to: if (progress === 1) ?
|
const progress = (!progressInfo.hasActiveTorrents || progressInfo.progress === 1)
|
||||||
if (!hasActiveTorrents || progress === 1) {
|
? -1
|
||||||
progress = -1
|
: progressInfo.progress
|
||||||
}
|
|
||||||
|
|
||||||
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
||||||
this.state.dock.progress = progress
|
this.state.dock.progress = progress
|
||||||
|
|
||||||
// Update progress for each individual torrent
|
// Update progress for each individual torrent
|
||||||
progressInfo.torrents.forEach((p) => {
|
progressInfo.torrents.forEach((p) => {
|
||||||
var torrentSummary = this.getTorrentSummary(p.torrentKey)
|
const torrentSummary = this.getTorrentSummary(p.torrentKey)
|
||||||
if (!torrentSummary) {
|
if (!torrentSummary) {
|
||||||
console.log('warning: got progress for missing torrent %s', p.torrentKey)
|
console.log('warning: got progress for missing torrent %s', p.torrentKey)
|
||||||
return
|
return
|
||||||
@@ -132,27 +126,27 @@ module.exports = class TorrentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
torrentFileModtimes (torrentKey, fileModtimes) {
|
torrentFileModtimes (torrentKey, fileModtimes) {
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.fileModtimes = fileModtimes
|
torrentSummary.fileModtimes = fileModtimes
|
||||||
dispatch('saveStateThrottled')
|
dispatch('saveStateThrottled')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentFileSaved (torrentKey, torrentFileName) {
|
torrentFileSaved (torrentKey, torrentFileName) {
|
||||||
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.torrentFileName = torrentFileName
|
torrentSummary.torrentFileName = torrentFileName
|
||||||
dispatch('saveStateThrottled')
|
dispatch('saveStateThrottled')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentPosterSaved (torrentKey, posterFileName) {
|
torrentPosterSaved (torrentKey, posterFileName) {
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.posterFileName = posterFileName
|
torrentSummary.posterFileName = posterFileName
|
||||||
dispatch('saveStateThrottled')
|
dispatch('saveStateThrottled')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentAudioMetadata (infoHash, index, info) {
|
torrentAudioMetadata (infoHash, index, info) {
|
||||||
var torrentSummary = this.getTorrentSummary(infoHash)
|
const torrentSummary = this.getTorrentSummary(infoHash)
|
||||||
var fileSummary = torrentSummary.files[index]
|
const fileSummary = torrentSummary.files[index]
|
||||||
fileSummary.audioInfo = info
|
fileSummary.audioInfo = info
|
||||||
dispatch('update')
|
dispatch('update')
|
||||||
}
|
}
|
||||||
@@ -169,7 +163,7 @@ module.exports = class TorrentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTorrentPath (torrentSummary) {
|
function getTorrentPath (torrentSummary) {
|
||||||
var itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
|
let itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
if (torrentSummary.files.length > 1) {
|
if (torrentSummary.files.length > 1) {
|
||||||
itemPath = path.dirname(itemPath)
|
itemPath = path.dirname(itemPath)
|
||||||
}
|
}
|
||||||
@@ -177,7 +171,7 @@ function getTorrentPath (torrentSummary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showDoneNotification (torrent) {
|
function showDoneNotification (torrent) {
|
||||||
var notif = new window.Notification('Download Complete', {
|
const notif = new window.Notification('Download Complete', {
|
||||||
body: torrent.name,
|
body: torrent.name,
|
||||||
silent: true
|
silent: true
|
||||||
})
|
})
|
||||||
@@ -186,5 +180,6 @@ function showDoneNotification (torrent) {
|
|||||||
ipcRenderer.send('show')
|
ipcRenderer.send('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
sound.play('DONE')
|
// Only play notification sound if player is inactive
|
||||||
|
if (this.state.playing.isPaused) sound.play('DONE')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ module.exports = class TorrentListController {
|
|||||||
this.state = state
|
this.state = state
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
// Adds a torrent to the list, starts downloading/seeding.
|
||||||
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// TorrentID can be a magnet URI, infohash, or torrent file: https://git.io/vik9M
|
||||||
addTorrent (torrentId) {
|
addTorrent (torrentId) {
|
||||||
if (torrentId.path) {
|
if (torrentId.path) {
|
||||||
// Use path string instead of W3C File object
|
// Use path string instead of W3C File object
|
||||||
torrentId = torrentId.path
|
torrentId = torrentId.path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow a instant.io link to be pasted
|
// Allow a instant.io link to be pasted
|
||||||
// TODO: remove this once support is added to webtorrent core
|
|
||||||
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||||
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var torrentKey = this.state.nextTorrentKey++
|
const torrentKey = this.state.nextTorrentKey++
|
||||||
var path = this.state.saved.prefs.downloadPath
|
const path = this.state.saved.prefs.downloadPath
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||||
|
|
||||||
@@ -40,12 +40,21 @@ module.exports = class TorrentListController {
|
|||||||
|
|
||||||
// Shows the Create Torrent page with options to seed a given file or folder
|
// Shows the Create Torrent page with options to seed a given file or folder
|
||||||
showCreateTorrent (files) {
|
showCreateTorrent (files) {
|
||||||
|
// You can only create torrents from the home screen.
|
||||||
|
if (this.state.location.url() !== 'home') {
|
||||||
|
return dispatch('error', 'Please go back to the torrent list before creating a new torrent.')
|
||||||
|
}
|
||||||
|
|
||||||
// Files will either be an array of file objects, which we can send directly
|
// Files will either be an array of file objects, which we can send directly
|
||||||
// to the create-torrent screen
|
// to the create-torrent screen
|
||||||
if (files.length === 0 || typeof files[0] !== 'string') {
|
if (files.length === 0 || typeof files[0] !== 'string') {
|
||||||
this.state.location.go({
|
this.state.location.go({
|
||||||
url: 'create-torrent',
|
url: 'create-torrent',
|
||||||
files: files
|
files: files,
|
||||||
|
setup: (cb) => {
|
||||||
|
this.state.window.title = 'Create New Torrent'
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -55,47 +64,56 @@ module.exports = class TorrentListController {
|
|||||||
findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles))
|
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
|
// Creates a new torrent and start seeeding
|
||||||
createTorrent (options) {
|
createTorrent (options) {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
var torrentKey = state.nextTorrentKey++
|
const torrentKey = state.nextTorrentKey++
|
||||||
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||||
state.location.backToFirst(function () {
|
state.location.cancel()
|
||||||
state.location.clearForward('create-torrent')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts downloading and/or seeding a given torrentSummary.
|
// Starts downloading and/or seeding a given torrentSummary.
|
||||||
startTorrentingSummary (torrentSummary) {
|
startTorrentingSummary (torrentKey) {
|
||||||
var s = torrentSummary
|
const s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
|
if (!s) throw new Error('Missing key: ' + torrentKey)
|
||||||
|
|
||||||
// Backward compatibility for config files save before we had torrentKey
|
// New torrent: give it a path
|
||||||
if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++
|
if (!s.path) {
|
||||||
|
// Use Downloads folder by default
|
||||||
|
s.path = this.state.saved.prefs.downloadPath
|
||||||
|
return start()
|
||||||
|
}
|
||||||
|
|
||||||
// Use Downloads folder by default
|
const fileOrFolder = TorrentSummary.getFileOrFolder(s)
|
||||||
if (!s.path) s.path = this.state.saved.prefs.downloadPath
|
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-torrenting',
|
// New torrent: metadata not yet received
|
||||||
s.torrentKey,
|
if (!fileOrFolder) return start()
|
||||||
TorrentSummary.getTorrentID(s),
|
|
||||||
s.path,
|
// Existing torrent: check that the path is still there
|
||||||
s.fileModtimes,
|
fs.stat(fileOrFolder, function (err) {
|
||||||
s.selections)
|
if (err) {
|
||||||
|
s.error = 'path-missing'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start()
|
||||||
|
})
|
||||||
|
|
||||||
|
function start () {
|
||||||
|
ipcRenderer.send('wt-start-torrenting',
|
||||||
|
s.torrentKey,
|
||||||
|
TorrentSummary.getTorrentID(s),
|
||||||
|
s.path,
|
||||||
|
s.fileModtimes,
|
||||||
|
s.selections)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
toggleTorrent (infoHash) {
|
toggleTorrent (infoHash) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
torrentSummary.status = 'new'
|
torrentSummary.status = 'new'
|
||||||
this.startTorrentingSummary(torrentSummary)
|
this.startTorrentingSummary(torrentSummary.torrentKey)
|
||||||
sound.play('ENABLE')
|
sound.play('ENABLE')
|
||||||
} else {
|
} else {
|
||||||
torrentSummary.status = 'paused'
|
torrentSummary.status = 'paused'
|
||||||
@@ -105,7 +123,7 @@ module.exports = class TorrentListController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleTorrentFile (infoHash, index) {
|
toggleTorrentFile (infoHash, index) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
||||||
|
|
||||||
// Let the WebTorrent process know to start or stop fetching that file
|
// Let the WebTorrent process know to start or stop fetching that file
|
||||||
@@ -124,14 +142,14 @@ module.exports = class TorrentListController {
|
|||||||
deleteTorrent (infoHash, deleteData) {
|
deleteTorrent (infoHash, deleteData) {
|
||||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
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) {
|
if (index > -1) {
|
||||||
var summary = this.state.saved.torrents[index]
|
const summary = this.state.saved.torrents[index]
|
||||||
|
|
||||||
// remove torrent and poster file
|
// remove torrent and poster file
|
||||||
deleteFile(TorrentSummary.getTorrentPath(summary))
|
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
|
// optionally delete the torrent data
|
||||||
if (deleteData) moveItemToTrash(summary)
|
if (deleteData) moveItemToTrash(summary)
|
||||||
@@ -141,7 +159,8 @@ module.exports = class TorrentListController {
|
|||||||
State.saveThrottled(this.state)
|
State.saveThrottled(this.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
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')
|
sound.play('DELETE')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,8 +173,8 @@ module.exports = class TorrentListController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openTorrentContextMenu (infoHash) {
|
openTorrentContextMenu (infoHash) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
var menu = new electron.remote.Menu()
|
const menu = new electron.remote.Menu()
|
||||||
|
|
||||||
menu.append(new electron.remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Remove From List',
|
label: 'Remove From List',
|
||||||
@@ -204,11 +223,11 @@ module.exports = class TorrentListController {
|
|||||||
// Calls `cb` on success, calls `onError` on failure
|
// Calls `cb` on success, calls `onError` on failure
|
||||||
function findFilesRecursive (paths, cb) {
|
function findFilesRecursive (paths, cb) {
|
||||||
if (paths.length > 1) {
|
if (paths.length > 1) {
|
||||||
var numComplete = 0
|
let numComplete = 0
|
||||||
var ret = []
|
let ret = []
|
||||||
paths.forEach(function (path) {
|
paths.forEach(function (path) {
|
||||||
findFilesRecursive([path], function (fileObjs) {
|
findFilesRecursive([path], function (fileObjs) {
|
||||||
ret = ret.concat(fileObjs)
|
ret.push(...fileObjs)
|
||||||
if (++numComplete === paths.length) {
|
if (++numComplete === paths.length) {
|
||||||
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
|
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
|
||||||
cb(ret)
|
cb(ret)
|
||||||
@@ -218,13 +237,13 @@ function findFilesRecursive (paths, cb) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileOrFolder = paths[0]
|
const fileOrFolder = paths[0]
|
||||||
fs.stat(fileOrFolder, function (err, stat) {
|
fs.stat(fileOrFolder, function (err, stat) {
|
||||||
if (err) return dispatch('error', err)
|
if (err) return dispatch('error', err)
|
||||||
|
|
||||||
// Files: return name, path, and size
|
// Files: return name, path, and size
|
||||||
if (!stat.isDirectory()) {
|
if (!stat.isDirectory()) {
|
||||||
var filePath = fileOrFolder
|
const filePath = fileOrFolder
|
||||||
return cb([{
|
return cb([{
|
||||||
name: path.basename(filePath),
|
name: path.basename(filePath),
|
||||||
path: filePath,
|
path: filePath,
|
||||||
@@ -233,10 +252,10 @@ function findFilesRecursive (paths, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Folders: recurse, make a list of all the files
|
// Folders: recurse, make a list of all the files
|
||||||
var folderPath = fileOrFolder
|
const folderPath = fileOrFolder
|
||||||
fs.readdir(folderPath, function (err, fileNames) {
|
fs.readdir(folderPath, function (err, fileNames) {
|
||||||
if (err) return dispatch('error', err)
|
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)
|
findFilesRecursive(paths, cb)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -251,8 +270,8 @@ function deleteFile (path) {
|
|||||||
|
|
||||||
// Delete all files in a torrent
|
// Delete all files in a torrent
|
||||||
function moveItemToTrash (torrentSummary) {
|
function moveItemToTrash (torrentSummary) {
|
||||||
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
const filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
ipcRenderer.send('moveItemToTrash', filePath)
|
if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function showItemInFolder (torrentSummary) {
|
function showItemInFolder (torrentSummary) {
|
||||||
@@ -260,9 +279,9 @@ function showItemInFolder (torrentSummary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveTorrentFileAs (torrentSummary) {
|
function saveTorrentFileAs (torrentSummary) {
|
||||||
var downloadPath = this.state.saved.prefs.downloadPath
|
const downloadPath = this.state.saved.prefs.downloadPath
|
||||||
var newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||||
var opts = {
|
const opts = {
|
||||||
title: 'Save Torrent File',
|
title: 'Save Torrent File',
|
||||||
defaultPath: path.join(downloadPath, newFileName),
|
defaultPath: path.join(downloadPath, newFileName),
|
||||||
filters: [
|
filters: [
|
||||||
@@ -270,8 +289,10 @@ function saveTorrentFileAs (torrentSummary) {
|
|||||||
{ name: 'All Files', extensions: ['*'] }
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
const win = electron.remote.getCurrentWindow()
|
||||||
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
|
||||||
|
if (!savePath) return // They clicked Cancel
|
||||||
|
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||||
if (err) return dispatch('error', err)
|
if (err) return dispatch('error', err)
|
||||||
fs.writeFile(savePath, torrentFile, function (err) {
|
fs.writeFile(savePath, torrentFile, function (err) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ module.exports = class UpdateController {
|
|||||||
|
|
||||||
// Shows a modal saying that we have an update
|
// Shows a modal saying that we have an update
|
||||||
updateAvailable (version) {
|
updateAvailable (version) {
|
||||||
var skipped = this.state.saved.skippedVersions
|
const skipped = this.state.saved.skippedVersions
|
||||||
if (skipped && skipped.includes(version)) {
|
if (skipped && skipped.includes(version)) {
|
||||||
console.log('new version skipped by user: v' + version)
|
console.log('new version skipped by user: v' + version)
|
||||||
return
|
return
|
||||||
@@ -18,7 +18,7 @@ module.exports = class UpdateController {
|
|||||||
|
|
||||||
// Don't show the modal again until the next version
|
// Don't show the modal again until the next version
|
||||||
skipVersion (version) {
|
skipVersion (version) {
|
||||||
var skipped = this.state.saved.skippedVersions
|
let skipped = this.state.saved.skippedVersions
|
||||||
if (!skipped) skipped = this.state.saved.skippedVersions = []
|
if (!skipped) skipped = this.state.saved.skippedVersions = []
|
||||||
skipped.push(version)
|
skipped.push(version)
|
||||||
State.saveThrottled(this.state)
|
State.saveThrottled(this.state)
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ function captureVideoFrame (video, format) {
|
|||||||
throw new Error('Second argument must be one of "png", "jpg", or "webp"')
|
throw new Error('Second argument must be one of "png", "jpg", or "webp"')
|
||||||
}
|
}
|
||||||
|
|
||||||
var canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
canvas.width = video.videoWidth
|
canvas.width = video.videoWidth
|
||||||
canvas.height = video.videoHeight
|
canvas.height = video.videoHeight
|
||||||
|
|
||||||
canvas.getContext('2d').drawImage(video, 0, 0)
|
canvas.getContext('2d').drawImage(video, 0, 0)
|
||||||
|
|
||||||
var dataUri = canvas.toDataURL('image/' + format)
|
const dataUri = canvas.toDataURL('image/' + format)
|
||||||
var data = dataUri.split(',')[1]
|
const data = dataUri.split(',')[1]
|
||||||
|
|
||||||
return new Buffer(data, 'base64')
|
return new Buffer(data, 'base64')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,19 +13,19 @@ module.exports = {
|
|||||||
setRate
|
setRate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy load these for a ~300ms improvement in startup time
|
const config = require('../../config')
|
||||||
var airplayer, chromecasts, dlnacasts
|
|
||||||
|
|
||||||
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
|
// 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
|
// Callback to notify module users when state has changed
|
||||||
var update
|
let update
|
||||||
|
|
||||||
// setInterval() for updating cast status
|
// setInterval() for updating cast status
|
||||||
var statusInterval = null
|
let statusInterval = null
|
||||||
|
|
||||||
// Start looking for cast devices on the local network
|
// Start looking for cast devices on the local network
|
||||||
function init (appState, callback) {
|
function init (appState, callback) {
|
||||||
@@ -59,7 +59,7 @@ function init (appState, callback) {
|
|||||||
|
|
||||||
// chromecast player implementation
|
// chromecast player implementation
|
||||||
function chromecastPlayer () {
|
function chromecastPlayer () {
|
||||||
var ret = {
|
const ret = {
|
||||||
device: null,
|
device: null,
|
||||||
addDevice,
|
addDevice,
|
||||||
getDevices,
|
getDevices,
|
||||||
@@ -95,8 +95,8 @@ function chromecastPlayer () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
ret.device.play(state.server.networkURL, {
|
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
title: config.APP_NAME + ' - ' + torrentSummary.name
|
title: config.APP_NAME + ' - ' + torrentSummary.name
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
@@ -146,7 +146,7 @@ function chromecastPlayer () {
|
|||||||
|
|
||||||
// airplay player implementation
|
// airplay player implementation
|
||||||
function airplayPlayer () {
|
function airplayPlayer () {
|
||||||
var ret = {
|
const ret = {
|
||||||
device: null,
|
device: null,
|
||||||
addDevice,
|
addDevice,
|
||||||
getDevices,
|
getDevices,
|
||||||
@@ -183,7 +183,7 @@ function airplayPlayer () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
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) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -238,7 +238,7 @@ function airplayPlayer () {
|
|||||||
|
|
||||||
// DLNA player implementation
|
// DLNA player implementation
|
||||||
function dlnaPlayer (player) {
|
function dlnaPlayer (player) {
|
||||||
var ret = {
|
const ret = {
|
||||||
device: null,
|
device: null,
|
||||||
addDevice,
|
addDevice,
|
||||||
getDevices,
|
getDevices,
|
||||||
@@ -274,8 +274,8 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
ret.device.play(state.server.networkURL, {
|
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
||||||
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
||||||
@@ -331,7 +331,7 @@ function dlnaPlayer (player) {
|
|||||||
// Start polling cast device state, whenever we're connected
|
// Start polling cast device state, whenever we're connected
|
||||||
function startStatusInterval () {
|
function startStatusInterval () {
|
||||||
statusInterval = setInterval(function () {
|
statusInterval = setInterval(function () {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.status()
|
if (player) player.status()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
@@ -350,12 +350,13 @@ function toggleMenu (location) {
|
|||||||
|
|
||||||
// Never cast to two devices at the same time
|
// Never cast to two devices at the same time
|
||||||
if (state.playing.location !== 'local') {
|
if (state.playing.location !== 'local') {
|
||||||
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
|
throw new Error('You can\'t connect to ' + location +
|
||||||
|
' when already connected to another device')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all cast devices of the given type
|
// Find all cast devices of the given type
|
||||||
var player = getPlayer(location)
|
const player = getPlayer(location)
|
||||||
var devices = player ? player.getDevices() : []
|
const devices = player ? player.getDevices() : []
|
||||||
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
||||||
|
|
||||||
// Show a menu
|
// Show a menu
|
||||||
@@ -363,10 +364,10 @@ function toggleMenu (location) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectDevice (index) {
|
function selectDevice (index) {
|
||||||
var {location, devices} = state.devices.castMenu
|
const {location, devices} = state.devices.castMenu
|
||||||
|
|
||||||
// Start casting
|
// Start casting
|
||||||
var player = getPlayer(location)
|
const player = getPlayer(location)
|
||||||
player.device = devices[index]
|
player.device = devices[index]
|
||||||
player.open()
|
player.open()
|
||||||
|
|
||||||
@@ -382,7 +383,7 @@ function selectDevice (index) {
|
|||||||
|
|
||||||
// Stops casting, move video back to local screen
|
// Stops casting, move video back to local screen
|
||||||
function stop () {
|
function stop () {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) {
|
if (player) {
|
||||||
player.stop(function () {
|
player.stop(function () {
|
||||||
player.device = null
|
player.device = null
|
||||||
@@ -396,7 +397,9 @@ function stop () {
|
|||||||
|
|
||||||
function stoppedCasting () {
|
function stoppedCasting () {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.playing.jumpToTime = state.playing.currentTime
|
state.playing.jumpToTime = Number.isFinite(state.playing.currentTime)
|
||||||
|
? state.playing.currentTime
|
||||||
|
: 0
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,18 +418,18 @@ function getPlayer (location) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play () {
|
function play () {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.play(castCallback)
|
if (player) player.play(castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause () {
|
function pause () {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.pause(castCallback)
|
if (player) player.pause(castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRate (rate) {
|
function setRate (rate) {
|
||||||
var player
|
let player
|
||||||
var result = true
|
let result = true
|
||||||
if (state.playing.location === 'chromecast') {
|
if (state.playing.location === 'chromecast') {
|
||||||
// TODO find how to control playback rate on chromecast
|
// TODO find how to control playback rate on chromecast
|
||||||
castCallback()
|
castCallback()
|
||||||
@@ -441,12 +444,12 @@ function setRate (rate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time) {
|
function seek (time) {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.seek(time, castCallback)
|
if (player) player.seek(time, castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVolume (volume) {
|
function setVolume (volume) {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.volume(volume, castCallback)
|
if (player) player.volume(volume, castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ module.exports = {
|
|||||||
setDispatch
|
setDispatch
|
||||||
}
|
}
|
||||||
|
|
||||||
var dispatchers = {}
|
const dispatchers = {}
|
||||||
var _dispatch = function () {}
|
let _dispatch = function () {}
|
||||||
|
|
||||||
function setDispatch (dispatch) {
|
function setDispatch (dispatch) {
|
||||||
_dispatch = dispatch
|
_dispatch = dispatch
|
||||||
@@ -20,8 +20,8 @@ function dispatch (...args) {
|
|||||||
// function. This prevents React from updating the listener functions on
|
// function. This prevents React from updating the listener functions on
|
||||||
// each update().
|
// each update().
|
||||||
function dispatcher (...args) {
|
function dispatcher (...args) {
|
||||||
var str = JSON.stringify(args)
|
const str = JSON.stringify(args)
|
||||||
var handler = dispatchers[str]
|
let handler = dispatchers[str]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
handler = dispatchers[str] = function (e) {
|
handler = dispatchers[str] = function (e) {
|
||||||
// Do not propagate click to elements below the button
|
// Do not propagate click to elements below the button
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
UnplayableError
|
UnplayableTorrentError,
|
||||||
|
UnplayableFileError
|
||||||
}
|
}
|
||||||
|
|
||||||
function UnplayableError () {
|
function UnplayableTorrentError () {
|
||||||
this.message = 'Can\'t play any files in torrent'
|
this.message = 'Can\'t play any files in torrent'
|
||||||
}
|
}
|
||||||
UnplayableError.prototype = Error
|
|
||||||
|
function UnplayableFileError () {
|
||||||
|
this.message = 'Can\'t play that file'
|
||||||
|
}
|
||||||
|
|
||||||
|
UnplayableTorrentError.prototype = Error
|
||||||
|
UnplayableFileError.prototype = Error
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ module.exports = {
|
|||||||
run
|
run
|
||||||
}
|
}
|
||||||
|
|
||||||
var semver = require('semver')
|
const semver = require('semver')
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
|
const TorrentSummary = require('./torrent-summary')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
// Change `state.saved` (which will be saved back to config.json on exit) as
|
// 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
|
// needed, for example to deal with config.json format changes across versions
|
||||||
@@ -15,7 +17,7 @@ function run (state) {
|
|||||||
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
|
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')) {
|
if (semver.lt(version, '0.7.0')) {
|
||||||
migrate_0_7_0(state.saved)
|
migrate_0_7_0(state.saved)
|
||||||
@@ -25,18 +27,28 @@ function run (state) {
|
|||||||
migrate_0_7_2(state.saved)
|
migrate_0_7_2(state.saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (semver.lt(version, '0.11.0')) {
|
||||||
|
migrate_0_11_0(state.saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semver.lt(version, '0.12.0')) {
|
||||||
|
migrate_0_12_0(state.saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semver.lt(version, '0.14.0')) {
|
||||||
|
migrate_0_14_0(state.saved)
|
||||||
|
}
|
||||||
|
|
||||||
// Config is now on the new version
|
// Config is now on the new version
|
||||||
state.saved.version = config.APP_VERSION
|
state.saved.version = config.APP_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_7_0 (saved) {
|
function migrate_0_7_0 (saved) {
|
||||||
console.log('migrate to 0.7.0')
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
var fs = require('fs-extra')
|
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
saved.torrents.forEach(function (ts) {
|
saved.torrents.forEach(function (ts) {
|
||||||
var infoHash = ts.infoHash
|
const infoHash = ts.infoHash
|
||||||
|
|
||||||
// Replace torrentPath with torrentFileName
|
// Replace torrentPath with torrentFileName
|
||||||
// There are a number of cases to handle here:
|
// There are a number of cases to handle here:
|
||||||
@@ -44,9 +56,8 @@ function migrate_0_7_0 (saved) {
|
|||||||
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
||||||
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
||||||
// * Finally, now we're getting rid of torrentPath altogether
|
// * Finally, now we're getting rid of torrentPath altogether
|
||||||
var src, dst
|
let src, dst
|
||||||
if (ts.torrentPath) {
|
if (ts.torrentPath) {
|
||||||
console.log('replacing torrentPath %s', ts.torrentPath)
|
|
||||||
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||||
src = ts.torrentPath
|
src = ts.torrentPath
|
||||||
} else {
|
} else {
|
||||||
@@ -63,8 +74,7 @@ function migrate_0_7_0 (saved) {
|
|||||||
|
|
||||||
// Replace posterURL with posterFileName
|
// Replace posterURL with posterFileName
|
||||||
if (ts.posterURL) {
|
if (ts.posterURL) {
|
||||||
console.log('replacing posterURL %s', ts.posterURL)
|
const extension = path.extname(ts.posterURL)
|
||||||
var extension = path.extname(ts.posterURL)
|
|
||||||
src = path.isAbsolute(ts.posterURL)
|
src = path.isAbsolute(ts.posterURL)
|
||||||
? ts.posterURL
|
? ts.posterURL
|
||||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||||
@@ -87,9 +97,51 @@ function migrate_0_7_0 (saved) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_7_2 (saved) {
|
function migrate_0_7_2 (saved) {
|
||||||
if (!saved.prefs) {
|
if (saved.prefs == null) {
|
||||||
saved.prefs = {
|
saved.prefs = {
|
||||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrate_0_11_0 (saved) {
|
||||||
|
if (saved.prefs.isFileHandler == null) {
|
||||||
|
// The app used to make itself the default torrent file handler automatically
|
||||||
|
saved.prefs.isFileHandler = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrate_0_12_0 (saved) {
|
||||||
|
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
|
||||||
|
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
|
||||||
|
}
|
||||||
|
delete saved.prefs.playInVlc
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const defaultTorrentFiles = [
|
||||||
|
'6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent',
|
||||||
|
'88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent',
|
||||||
|
'6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent',
|
||||||
|
'02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent',
|
||||||
|
'3ba219a8634bf7bae3d848192b2da75ae995589d.torrent'
|
||||||
|
]
|
||||||
|
saved.torrents.forEach(function (torrentSummary) {
|
||||||
|
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
|
||||||
|
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
|
if (!fileOrFolder) return
|
||||||
|
try {
|
||||||
|
fs.statSync(fileOrFolder)
|
||||||
|
} catch (e) {
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
85
src/renderer/lib/playlist.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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.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 (var 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 (var i = state.playing.fileIndex + 1; i < files.length; i++) {
|
||||||
|
if (TorrentPlayer.isPlayable(files[i])) return i
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -3,15 +3,15 @@ module.exports = {
|
|||||||
play
|
play
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
var VOLUME = 0.15
|
const VOLUME = 0.15
|
||||||
|
|
||||||
/* Cache of Audio elements, for instant playback */
|
/* Cache of Audio elements, for instant playback */
|
||||||
var cache = {}
|
const cache = {}
|
||||||
|
|
||||||
var sounds = {
|
const sounds = {
|
||||||
ADD: {
|
ADD: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
|
||||||
volume: VOLUME
|
volume: VOLUME
|
||||||
@@ -47,10 +47,10 @@ var sounds = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function preload () {
|
function preload () {
|
||||||
for (var name in sounds) {
|
for (let name in sounds) {
|
||||||
if (!cache[name]) {
|
if (!cache[name]) {
|
||||||
var sound = sounds[name]
|
const sound = sounds[name]
|
||||||
var audio = cache[name] = new window.Audio()
|
const audio = cache[name] = new window.Audio()
|
||||||
audio.volume = sound.volume
|
audio.volume = sound.volume
|
||||||
audio.src = sound.url
|
audio.src = sound.url
|
||||||
}
|
}
|
||||||
@@ -58,9 +58,9 @@ function preload () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play (name) {
|
function play (name) {
|
||||||
var audio = cache[name]
|
let audio = cache[name]
|
||||||
if (!audio) {
|
if (!audio) {
|
||||||
var sound = sounds[name]
|
const sound = sounds[name]
|
||||||
if (!sound) {
|
if (!sound) {
|
||||||
throw new Error('Invalid sound name')
|
throw new Error('Invalid sound name')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
var appConfig = require('application-config')('WebTorrent')
|
const appConfig = require('application-config')('WebTorrent')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var {EventEmitter} = require('events')
|
const {EventEmitter} = require('events')
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var migrations = require('./migrations')
|
const migrations = require('./migrations')
|
||||||
|
|
||||||
var State = module.exports = Object.assign(new EventEmitter(), {
|
const State = module.exports = Object.assign(new EventEmitter(), {
|
||||||
getDefaultPlayState,
|
getDefaultPlayState,
|
||||||
load,
|
load,
|
||||||
save,
|
save,
|
||||||
@@ -15,7 +15,7 @@ var State = module.exports = Object.assign(new EventEmitter(), {
|
|||||||
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||||
|
|
||||||
function getDefaultState () {
|
function getDefaultState () {
|
||||||
var LocationHistory = require('location-history')
|
const LocationHistory = require('location-history')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/*
|
/*
|
||||||
@@ -24,7 +24,11 @@ function getDefaultState () {
|
|||||||
*/
|
*/
|
||||||
client: null, /* the WebTorrent client */
|
client: null, /* the WebTorrent client */
|
||||||
server: null, /* local WebTorrent-to-HTTP server */
|
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(),
|
location: new LocationHistory(),
|
||||||
window: {
|
window: {
|
||||||
bounds: null, /* {x, y, width, height } */
|
bounds: null, /* {x, y, width, height } */
|
||||||
@@ -94,22 +98,25 @@ function getDefaultPlayState () {
|
|||||||
|
|
||||||
/* If the saved state file doesn't exist yet, here's what we use instead */
|
/* If the saved state file doesn't exist yet, here's what we use instead */
|
||||||
function setupSavedState (cb) {
|
function setupSavedState (cb) {
|
||||||
var fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
var parseTorrent = require('parse-torrent')
|
const parseTorrent = require('parse-torrent')
|
||||||
var parallel = require('run-parallel')
|
const parallel = require('run-parallel')
|
||||||
|
|
||||||
var saved = {
|
const saved = {
|
||||||
prefs: {
|
prefs: {
|
||||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
||||||
|
isFileHandler: false,
|
||||||
|
openExternalPlayer: false,
|
||||||
|
externalPlayerPath: null
|
||||||
},
|
},
|
||||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||||
}
|
}
|
||||||
|
|
||||||
var tasks = []
|
const tasks = []
|
||||||
|
|
||||||
config.DEFAULT_TORRENTS.map(function (t, i) {
|
config.DEFAULT_TORRENTS.map(function (t, i) {
|
||||||
var infoHash = saved.torrents[i].infoHash
|
const infoHash = saved.torrents[i].infoHash
|
||||||
tasks.push(function (cb) {
|
tasks.push(function (cb) {
|
||||||
fs.copy(
|
fs.copy(
|
||||||
path.join(config.STATIC_PATH, t.posterFileName),
|
path.join(config.STATIC_PATH, t.posterFileName),
|
||||||
@@ -132,8 +139,8 @@ function setupSavedState (cb) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function createTorrentObject (t) {
|
function createTorrentObject (t) {
|
||||||
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
||||||
var parsedTorrent = parseTorrent(torrent)
|
const parsedTorrent = parseTorrent(torrent)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'paused',
|
status: 'paused',
|
||||||
@@ -150,18 +157,18 @@ function setupSavedState (cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPlayingTorrentSummary () {
|
function getPlayingTorrentSummary () {
|
||||||
var infoHash = this.playing.infoHash
|
const infoHash = this.playing.infoHash
|
||||||
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayingFileSummary () {
|
function getPlayingFileSummary () {
|
||||||
var torrentSummary = this.getPlayingTorrentSummary()
|
const torrentSummary = this.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary) return null
|
if (!torrentSummary) return null
|
||||||
return torrentSummary.files[this.playing.fileIndex]
|
return torrentSummary.files[this.playing.fileIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
function load (cb) {
|
function load (cb) {
|
||||||
var state = getDefaultState()
|
const state = getDefaultState()
|
||||||
|
|
||||||
appConfig.read(function (err, saved) {
|
appConfig.read(function (err, saved) {
|
||||||
if (err || !saved.version) {
|
if (err || !saved.version) {
|
||||||
@@ -186,20 +193,23 @@ function save (state, cb) {
|
|||||||
delete state.saveStateTimeout
|
delete state.saveStateTimeout
|
||||||
|
|
||||||
// Clean up, so that we're not saving any pending state
|
// 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
|
// 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
|
// reading the torrent file or file(s) to seed & don't have an infohash
|
||||||
copy.torrents = copy.torrents
|
copy.torrents = copy.torrents
|
||||||
.filter((x) => x.infoHash)
|
.filter((x) => x.infoHash)
|
||||||
.map(function (x) {
|
.map(function (x) {
|
||||||
var torrent = {}
|
const torrent = {}
|
||||||
for (var key in x) {
|
for (let key in x) {
|
||||||
if (key === 'progress' || key === 'torrentKey') {
|
if (key === 'progress' || key === 'torrentKey') {
|
||||||
continue // Don't save progress info or key for the webtorrent process
|
continue // Don't save progress info or key for the webtorrent process
|
||||||
}
|
}
|
||||||
if (key === 'playStatus') {
|
if (key === 'playStatus') {
|
||||||
continue // Don't save whether a torrent is playing / pending
|
continue // Don't save whether a torrent is playing / pending
|
||||||
}
|
}
|
||||||
|
if (key === 'error') {
|
||||||
|
continue // Don't save error states
|
||||||
|
}
|
||||||
torrent[key] = x[key]
|
torrent[key] = x[key]
|
||||||
}
|
}
|
||||||
return torrent
|
return torrent
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const url = require('url')
|
|||||||
|
|
||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
|
|
||||||
var telemetry
|
let telemetry
|
||||||
|
|
||||||
function init (state) {
|
function init (state) {
|
||||||
telemetry = state.saved.telemetry
|
telemetry = state.saved.telemetry
|
||||||
@@ -23,7 +23,8 @@ function init (state) {
|
|||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = new Date()
|
const now = new Date()
|
||||||
|
telemetry.version = config.APP_VERSION
|
||||||
telemetry.timestamp = now.toISOString()
|
telemetry.timestamp = now.toISOString()
|
||||||
telemetry.localTime = now.toTimeString()
|
telemetry.localTime = now.toTimeString()
|
||||||
telemetry.screens = getScreenInfo()
|
telemetry.screens = getScreenInfo()
|
||||||
@@ -32,6 +33,8 @@ function init (state) {
|
|||||||
|
|
||||||
if (config.IS_PRODUCTION) {
|
if (config.IS_PRODUCTION) {
|
||||||
postToServer()
|
postToServer()
|
||||||
|
// If the user keeps WebTorrent running for a long time, post every 12h
|
||||||
|
setInterval(postToServer, 12 * 3600 * 1000)
|
||||||
} else {
|
} else {
|
||||||
// Development: telemetry used only for local debugging
|
// Development: telemetry used only for local debugging
|
||||||
// Empty uncaught errors, etc at the start of every run
|
// Empty uncaught errors, etc at the start of every run
|
||||||
@@ -42,6 +45,7 @@ function init (state) {
|
|||||||
function reset () {
|
function reset () {
|
||||||
telemetry.uncaughtErrors = []
|
telemetry.uncaughtErrors = []
|
||||||
telemetry.playAttempts = {
|
telemetry.playAttempts = {
|
||||||
|
minVersion: config.APP_VERSION,
|
||||||
total: 0,
|
total: 0,
|
||||||
success: 0,
|
success: 0,
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
@@ -52,17 +56,17 @@ function reset () {
|
|||||||
|
|
||||||
function postToServer () {
|
function postToServer () {
|
||||||
// Serialize the telemetry summary
|
// Serialize the telemetry summary
|
||||||
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
|
const payload = new Buffer(JSON.stringify(telemetry), 'utf8')
|
||||||
|
|
||||||
// POST to our server
|
// POST to our server
|
||||||
var options = url.parse(config.TELEMETRY_URL)
|
const options = url.parse(config.TELEMETRY_URL)
|
||||||
options.method = 'POST'
|
options.method = 'POST'
|
||||||
options.headers = {
|
options.headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Content-Length': payload.length
|
'Content-Length': payload.length
|
||||||
}
|
}
|
||||||
|
|
||||||
var req = https.request(options, function (res) {
|
const req = https.request(options, function (res) {
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
console.log('Successfully posted telemetry summary')
|
console.log('Successfully posted telemetry summary')
|
||||||
reset()
|
reset()
|
||||||
@@ -81,7 +85,7 @@ function postToServer () {
|
|||||||
// collects screen resolution, etc
|
// collects screen resolution, etc
|
||||||
function createSummary () {
|
function createSummary () {
|
||||||
// Make a 256-bit random unique ID
|
// Make a 256-bit random unique ID
|
||||||
var userID = crypto.randomBytes(32).toString('hex')
|
const userID = crypto.randomBytes(32).toString('hex')
|
||||||
return { userID }
|
return { userID }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,36 +111,73 @@ function getSystemInfo () {
|
|||||||
|
|
||||||
// Get the number of torrents, rounded to the nearest power of two
|
// Get the number of torrents, rounded to the nearest power of two
|
||||||
function getApproxNumTorrents (state) {
|
function getApproxNumTorrents (state) {
|
||||||
var exactNum = state.saved.torrents.length
|
const exactNum = state.saved.torrents.length
|
||||||
if (exactNum === 0) return 0
|
if (exactNum === 0) return 0
|
||||||
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||||
var log2 = Math.log(exactNum) / Math.log(2)
|
const log2 = Math.log(exactNum) / Math.log(2)
|
||||||
return 1 << Math.round(log2)
|
return 1 << Math.round(log2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// An uncaught error happened in the main process or in one of the windows
|
// An uncaught error happened in the main process or in one of the windows
|
||||||
function logUncaughtError (procName, err) {
|
function logUncaughtError (procName, e) {
|
||||||
console.error('uncaught error', procName, err)
|
|
||||||
|
|
||||||
// Not initialized yet? Ignore.
|
// Not initialized yet? Ignore.
|
||||||
// Hopefully uncaught errors immediately on startup are fixed in dev
|
// Hopefully uncaught errors immediately on startup are fixed in dev
|
||||||
if (!telemetry) return
|
if (!telemetry) return
|
||||||
|
|
||||||
var message, stack
|
let message
|
||||||
if (err instanceof Error) {
|
let stack = ''
|
||||||
message = err.message
|
if (e == null) {
|
||||||
stack = err.stack
|
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 {
|
} else {
|
||||||
message = String(err)
|
// Resource errors (captured element.onerror), err is an Event
|
||||||
stack = ''
|
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
|
// We need to POST the telemetry object, make sure it stays < 100kb
|
||||||
if (telemetry.uncaughtErrors.length > 20) return
|
if (telemetry.uncaughtErrors.length > 20) return
|
||||||
if (message.length > 1000) message = message.substring(0, 1000)
|
if (message.length > 1000) message = message.substring(0, 1000)
|
||||||
if (stack.length > 1000) stack = stack.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 (e) {}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user pressed play. It either worked, timed out, or showed the
|
// The user pressed play. It either worked, timed out, or showed the
|
||||||
@@ -146,7 +187,7 @@ function logPlayAttempt (result) {
|
|||||||
return console.error('Unknown play attempt result', result)
|
return console.error('Unknown play attempt result', result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var attempts = telemetry.playAttempts
|
const attempts = telemetry.playAttempts
|
||||||
attempts.total = (attempts.total || 0) + 1
|
attempts.total = (attempts.total || 0) + 1
|
||||||
attempts[result] = (attempts[result] || 0) + 1
|
attempts[result] = (attempts[result] || 0) + 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ module.exports = {
|
|||||||
isVideo,
|
isVideo,
|
||||||
isAudio,
|
isAudio,
|
||||||
isTorrent,
|
isTorrent,
|
||||||
isPlayableTorrentSummary,
|
isPlayableTorrentSummary
|
||||||
pickFileToPlay
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
// Checks whether a fileSummary or file path is audio/video that we can play,
|
// Checks whether a fileSummary or file path is audio/video that we can play,
|
||||||
// based on the file extension
|
// based on the file extension
|
||||||
@@ -37,7 +36,8 @@ function isAudio (file) {
|
|||||||
'.ac3',
|
'.ac3',
|
||||||
'.mp3',
|
'.mp3',
|
||||||
'.ogg',
|
'.ogg',
|
||||||
'.wav'
|
'.wav',
|
||||||
|
'.m4a'
|
||||||
].includes(getFileExtension(file))
|
].includes(getFileExtension(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,38 +46,16 @@ function isAudio (file) {
|
|||||||
// - a file object where obj.name is ends in .torrent
|
// - a file object where obj.name is ends in .torrent
|
||||||
// - a string that's a magnet link (magnet://...)
|
// - a string that's a magnet link (magnet://...)
|
||||||
function isTorrent (file) {
|
function isTorrent (file) {
|
||||||
var isTorrentFile = getFileExtension(file) === '.torrent'
|
const isTorrentFile = getFileExtension(file) === '.torrent'
|
||||||
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
const isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
||||||
return isTorrentFile || isMagnet
|
return isTorrentFile || isMagnet
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileExtension (file) {
|
function getFileExtension (file) {
|
||||||
var name = typeof file === 'string' ? file : file.name
|
const name = typeof file === 'string' ? file : file.name
|
||||||
return path.extname(name).toLowerCase()
|
return path.extname(name).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlayableTorrentSummary (torrentSummary) {
|
function isPlayableTorrentSummary (torrentSummary) {
|
||||||
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
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
|
module.exports = torrentPoster
|
||||||
|
|
||||||
var captureVideoFrame = require('./capture-video-frame')
|
const captureVideoFrame = require('./capture-video-frame')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
function torrentPoster (torrent, cb) {
|
function torrentPoster (torrent, cb) {
|
||||||
// First, try to use a poster image if available
|
// 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)
|
return /^poster\.(jpg|png|gif)$/.test(file.name)
|
||||||
})[0]
|
})[0]
|
||||||
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
|
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
|
||||||
|
|
||||||
// Second, try to use the largest video file
|
// Second, try to use the largest video file
|
||||||
// Filter out file formats that the <video> tag definitely can't play
|
// Filter out file formats that the <video> tag definitely can't play
|
||||||
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
const videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
||||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||||
|
|
||||||
// Third, try to use the largest image file
|
// 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)
|
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
||||||
|
|
||||||
// TODO: generate a waveform from the largest sound file
|
// TODO: generate a waveform from the largest sound file
|
||||||
@@ -25,8 +25,8 @@ function torrentPoster (torrent, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLargestFileByExtension (torrent, extensions) {
|
function getLargestFileByExtension (torrent, extensions) {
|
||||||
var files = torrent.files.filter(function (file) {
|
const files = torrent.files.filter(function (file) {
|
||||||
var extname = path.extname(file.name).toLowerCase()
|
const extname = path.extname(file.name).toLowerCase()
|
||||||
return extensions.indexOf(extname) !== -1
|
return extensions.indexOf(extname) !== -1
|
||||||
})
|
})
|
||||||
if (files.length === 0) return undefined
|
if (files.length === 0) return undefined
|
||||||
@@ -36,15 +36,15 @@ function getLargestFileByExtension (torrent, extensions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function torrentPosterFromVideo (file, torrent, cb) {
|
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)
|
server.listen(0, onListening)
|
||||||
|
|
||||||
function onListening () {
|
function onListening () {
|
||||||
var port = server.address().port
|
const port = server.address().port
|
||||||
var url = 'http://localhost:' + port + '/' + index
|
const url = 'http://localhost:' + port + '/' + index
|
||||||
var video = document.createElement('video')
|
const video = document.createElement('video')
|
||||||
video.addEventListener('canplay', onCanPlay)
|
video.addEventListener('canplay', onCanPlay)
|
||||||
|
|
||||||
video.volume = 0
|
video.volume = 0
|
||||||
@@ -61,7 +61,7 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
|||||||
function onSeeked () {
|
function onSeeked () {
|
||||||
video.removeEventListener('seeked', onSeeked)
|
video.removeEventListener('seeked', onSeeked)
|
||||||
|
|
||||||
var buf = captureVideoFrame(video)
|
const buf = captureVideoFrame(video)
|
||||||
|
|
||||||
// unload video element
|
// unload video element
|
||||||
video.pause()
|
video.pause()
|
||||||
@@ -78,6 +78,6 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function torrentPosterFromImage (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))
|
file.getBuffer((err, buf) => cb(err, buf, extname))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ module.exports = {
|
|||||||
getFileOrFolder
|
getFileOrFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
|
|
||||||
// Expects a torrentSummary
|
// Expects a torrentSummary
|
||||||
// Returns an absolute path to the torrent file, or null if unavailable
|
// 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
|
// Returns an absolute path to the poster image, or null if unavailable
|
||||||
function getPosterPath (torrentSummary) {
|
function getPosterPath (torrentSummary) {
|
||||||
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
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):
|
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
||||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||||
return posterPath.replace(/\\/g, '/')
|
return posterPath.replace(/\\/g, '/')
|
||||||
@@ -29,7 +29,7 @@ function getPosterPath (torrentSummary) {
|
|||||||
// Expects a torrentSummary
|
// Expects a torrentSummary
|
||||||
// Returns a torrentID: filename, magnet URI, or infohash
|
// Returns a torrentID: filename, magnet URI, or infohash
|
||||||
function getTorrentID (torrentSummary) {
|
function getTorrentID (torrentSummary) {
|
||||||
var s = torrentSummary
|
const s = torrentSummary
|
||||||
if (s.torrentFileName) { // Load torrent file from disk
|
if (s.torrentFileName) { // Load torrent file from disk
|
||||||
return getTorrentPath(s)
|
return getTorrentPath(s)
|
||||||
} else { // Load torrent from DHT
|
} else { // Load torrent from DHT
|
||||||
@@ -51,6 +51,7 @@ function getByKey (state, torrentKey) {
|
|||||||
// TODO: make this assumption explicit, enforce it in the `create-torrent`
|
// TODO: make this assumption explicit, enforce it in the `create-torrent`
|
||||||
// module. Store root folder explicitly to avoid hacky path processing below.
|
// module. Store root folder explicitly to avoid hacky path processing below.
|
||||||
function getFileOrFolder (torrentSummary) {
|
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])
|
return path.join(ts.path, ts.files[0].path.split('/')[0])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,21 @@ crashReporter.init()
|
|||||||
|
|
||||||
const dragDrop = require('drag-drop')
|
const dragDrop = require('drag-drop')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
const fs = require('fs')
|
||||||
const React = require('react')
|
const React = require('react')
|
||||||
const ReactDOM = require('react-dom')
|
const ReactDOM = require('react-dom')
|
||||||
|
|
||||||
const config = require('../config')
|
const config = require('../config')
|
||||||
const App = require('./views/app')
|
|
||||||
const telemetry = require('./lib/telemetry')
|
const telemetry = require('./lib/telemetry')
|
||||||
const sound = require('./lib/sound')
|
const sound = require('./lib/sound')
|
||||||
const State = require('./lib/state')
|
const State = require('./lib/state')
|
||||||
const TorrentPlayer = require('./lib/torrent-player')
|
const TorrentPlayer = require('./lib/torrent-player')
|
||||||
|
|
||||||
|
// Required by Material UI -- adds `onTouchTap` event
|
||||||
|
require('react-tap-event-plugin')()
|
||||||
|
|
||||||
|
const App = require('./pages/app')
|
||||||
|
|
||||||
const MediaController = require('./controllers/media-controller')
|
const MediaController = require('./controllers/media-controller')
|
||||||
const UpdateController = require('./controllers/update-controller')
|
const UpdateController = require('./controllers/update-controller')
|
||||||
const PrefsController = require('./controllers/prefs-controller')
|
const PrefsController = require('./controllers/prefs-controller')
|
||||||
@@ -23,27 +28,27 @@ const SubtitlesController = require('./controllers/subtitles-controller')
|
|||||||
const TorrentListController = require('./controllers/torrent-list-controller')
|
const TorrentListController = require('./controllers/torrent-list-controller')
|
||||||
const TorrentController = require('./controllers/torrent-controller')
|
const TorrentController = require('./controllers/torrent-controller')
|
||||||
|
|
||||||
|
// 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.
|
// Yo-yo pattern: state object lives here and percolates down thru all the views.
|
||||||
// Events come back up from the views via dispatch(...)
|
// Events come back up from the views via dispatch(...)
|
||||||
require('./lib/dispatcher').setDispatch(dispatch)
|
require('./lib/dispatcher').setDispatch(dispatch)
|
||||||
|
|
||||||
// From dispatch(...), events are sent to one of the controllers
|
// 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
|
// This dependency is the slowest-loading, so we lazy load it
|
||||||
var Cast = null
|
let 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
|
|
||||||
|
|
||||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
// 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.
|
// All other state is ephemeral. First we load state.saved then initialize the app.
|
||||||
var state
|
let state
|
||||||
|
|
||||||
// Root React component
|
// Root React component
|
||||||
var app
|
let app
|
||||||
|
|
||||||
State.load(onState)
|
State.load(onState)
|
||||||
|
|
||||||
@@ -54,6 +59,13 @@ function onState (err, _state) {
|
|||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
state = window.state = _state // Make available for easier debugging
|
state = window.state = _state // Make available for easier debugging
|
||||||
|
|
||||||
|
telemetry.init(state)
|
||||||
|
|
||||||
|
// Log uncaught JS errors
|
||||||
|
window.addEventListener('error',
|
||||||
|
(e) => telemetry.logUncaughtError('window', e),
|
||||||
|
true /* capture */)
|
||||||
|
|
||||||
// Create controllers
|
// Create controllers
|
||||||
controllers = {
|
controllers = {
|
||||||
media: new MediaController(state),
|
media: new MediaController(state),
|
||||||
@@ -77,17 +89,20 @@ function onState (err, _state) {
|
|||||||
// Restart everything we were torrenting last time the app ran
|
// Restart everything we were torrenting last time the app ran
|
||||||
resumeTorrents()
|
resumeTorrents()
|
||||||
|
|
||||||
|
// 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
|
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
||||||
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
|
|
||||||
// Listen for messages from the main process
|
// Listen for messages from the main process
|
||||||
setupIpc()
|
setupIpc()
|
||||||
|
|
||||||
// Calling update() updates the UI given the current state
|
// Warn if the download dir is gone, eg b/c an external drive is unplugged
|
||||||
// Do this at least once a second to give every file in every torrentSummary
|
checkDownloadPath()
|
||||||
// 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'))
|
|
||||||
|
|
||||||
// OS integrations:
|
// OS integrations:
|
||||||
// ...drag and drop files/text to start torrenting or seeding
|
// ...drag and drop files/text to start torrenting or seeding
|
||||||
@@ -106,10 +121,6 @@ function onState (err, _state) {
|
|||||||
// ...window visibility state.
|
// ...window visibility state.
|
||||||
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
||||||
|
|
||||||
// Log uncaught JS errors
|
|
||||||
window.addEventListener('error',
|
|
||||||
(e) => telemetry.logUncaughtError('window', e.error || e.target), true)
|
|
||||||
|
|
||||||
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||||
sound.play('STARTUP')
|
sound.play('STARTUP')
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
@@ -119,7 +130,6 @@ function onState (err, _state) {
|
|||||||
function delayedInit () {
|
function delayedInit () {
|
||||||
lazyLoadCast()
|
lazyLoadCast()
|
||||||
sound.preload()
|
sound.preload()
|
||||||
telemetry.init(state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazily loads Chromecast and Airplay support
|
// Lazily loads Chromecast and Airplay support
|
||||||
@@ -149,7 +159,7 @@ function updateElectron () {
|
|||||||
state.prev.title = state.window.title
|
state.prev.title = state.window.title
|
||||||
ipcRenderer.send('setTitle', 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
|
state.prev.progress = state.dock.progress
|
||||||
ipcRenderer.send('setProgress', state.dock.progress)
|
ipcRenderer.send('setProgress', state.dock.progress)
|
||||||
}
|
}
|
||||||
@@ -167,20 +177,26 @@ const dispatchHandlers = {
|
|||||||
|
|
||||||
'addTorrent': (torrentId) => controllers.torrentList.addTorrent(torrentId),
|
'addTorrent': (torrentId) => controllers.torrentList.addTorrent(torrentId),
|
||||||
'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths),
|
'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths),
|
||||||
'toggleCreateTorrentAdvanced': () => controllers.torrentList.toggleCreateTorrentAdvanced(),
|
|
||||||
'createTorrent': (options) => controllers.torrentList.createTorrent(options),
|
'createTorrent': (options) => controllers.torrentList.createTorrent(options),
|
||||||
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
|
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
|
||||||
'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index),
|
'toggleTorrentFile': (infoHash, index) =>
|
||||||
'confirmDeleteTorrent': (infoHash, deleteData) => controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData),
|
controllers.torrentList.toggleTorrentFile(infoHash, index),
|
||||||
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
'confirmDeleteTorrent': (infoHash, deleteData) =>
|
||||||
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData),
|
||||||
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
'deleteTorrent': (infoHash, deleteData) =>
|
||||||
'startTorrentingSummary': (torrentSummary) =>
|
controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
||||||
controllers.torrentList.startTorrentingSummary(torrentSummary),
|
'toggleSelectTorrent': (infoHash) =>
|
||||||
|
controllers.torrentList.toggleSelectTorrent(infoHash),
|
||||||
|
'openTorrentContextMenu': (infoHash) =>
|
||||||
|
controllers.torrentList.openTorrentContextMenu(infoHash),
|
||||||
|
'startTorrentingSummary': (torrentKey) =>
|
||||||
|
controllers.torrentList.startTorrentingSummary(torrentKey),
|
||||||
|
|
||||||
// Playback
|
// Playback
|
||||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
||||||
'playPause': () => controllers.playback.playPause(),
|
'playPause': () => controllers.playback.playPause(),
|
||||||
|
'nextTrack': () => controllers.playback.nextTrack(),
|
||||||
|
'previousTrack': () => controllers.playback.previousTrack(),
|
||||||
'skip': (time) => controllers.playback.skip(time),
|
'skip': (time) => controllers.playback.skip(time),
|
||||||
'skipTo': (time) => controllers.playback.skipTo(time),
|
'skipTo': (time) => controllers.playback.skipTo(time),
|
||||||
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
|
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
|
||||||
@@ -195,14 +211,14 @@ const dispatchHandlers = {
|
|||||||
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),
|
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),
|
||||||
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect),
|
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect),
|
||||||
|
|
||||||
// Local media: <video>, <audio>, VLC
|
// Local media: <video>, <audio>, external players
|
||||||
'mediaStalled': () => controllers.media.mediaStalled(),
|
'mediaStalled': () => controllers.media.mediaStalled(),
|
||||||
'mediaError': (err) => controllers.media.mediaError(err),
|
'mediaError': (err) => controllers.media.mediaError(err),
|
||||||
'mediaSuccess': () => controllers.media.mediaSuccess(),
|
'mediaSuccess': () => controllers.media.mediaSuccess(),
|
||||||
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
|
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
|
||||||
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
|
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
|
||||||
'vlcPlay': () => controllers.media.vlcPlay(),
|
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
|
||||||
'vlcNotFound': () => controllers.media.vlcNotFound(),
|
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
|
||||||
|
|
||||||
// Remote casting: Chromecast, Airplay, etc
|
// Remote casting: Chromecast, Airplay, etc
|
||||||
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
|
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
|
||||||
@@ -212,6 +228,7 @@ const dispatchHandlers = {
|
|||||||
// Preferences screen
|
// Preferences screen
|
||||||
'preferences': () => controllers.prefs.show(),
|
'preferences': () => controllers.prefs.show(),
|
||||||
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
|
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
|
||||||
|
'checkDownloadPath': checkDownloadPath,
|
||||||
|
|
||||||
// Update (check for new versions on Linux, where there's no auto updater)
|
// Update (check for new versions on Linux, where there's no auto updater)
|
||||||
'updateAvailable': (version) => controllers.update.updateAvailable(version),
|
'updateAvailable': (version) => controllers.update.updateAvailable(version),
|
||||||
@@ -223,6 +240,7 @@ const dispatchHandlers = {
|
|||||||
'escapeBack': escapeBack,
|
'escapeBack': escapeBack,
|
||||||
'back': () => state.location.back(),
|
'back': () => state.location.back(),
|
||||||
'forward': () => state.location.forward(),
|
'forward': () => state.location.forward(),
|
||||||
|
'cancel': () => state.location.cancel(),
|
||||||
|
|
||||||
// Controlling the window
|
// Controlling the window
|
||||||
'setDimensions': setDimensions,
|
'setDimensions': setDimensions,
|
||||||
@@ -246,7 +264,7 @@ function dispatch (action, ...args) {
|
|||||||
console.log('dispatch: %s %o', action, args)
|
console.log('dispatch: %s %o', action, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
var handler = dispatchHandlers[action]
|
const handler = dispatchHandlers[action]
|
||||||
if (handler) handler(...args)
|
if (handler) handler(...args)
|
||||||
else console.error('Missing dispatch handler: ' + action)
|
else console.error('Missing dispatch handler: ' + action)
|
||||||
|
|
||||||
@@ -266,7 +284,7 @@ function setupIpc () {
|
|||||||
|
|
||||||
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
|
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
|
||||||
|
|
||||||
var tc = controllers.torrent
|
const tc = controllers.torrent
|
||||||
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
|
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
|
||||||
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
|
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
|
||||||
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
|
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
|
||||||
@@ -293,7 +311,7 @@ function backToList () {
|
|||||||
state.modal = null
|
state.modal = null
|
||||||
state.location.backToFirst(function () {
|
state.location.backToFirst(function () {
|
||||||
// If we were already on the torrent list, scroll to the top
|
// 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
|
if (contentTag) contentTag.scrollTop = 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -312,8 +330,14 @@ function escapeBack () {
|
|||||||
// Starts all torrents that aren't paused on program startup
|
// Starts all torrents that aren't paused on program startup
|
||||||
function resumeTorrents () {
|
function resumeTorrents () {
|
||||||
state.saved.torrents
|
state.saved.torrents
|
||||||
.filter((torrentSummary) => torrentSummary.status !== 'paused')
|
.map((torrentSummary) => {
|
||||||
.forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary))
|
// Torrent keys are ephemeral, reassigned each time the app runs.
|
||||||
|
// On startup, give all torrents a key, even the ones that are paused.
|
||||||
|
torrentSummary.torrentKey = state.nextTorrentKey++
|
||||||
|
return torrentSummary
|
||||||
|
})
|
||||||
|
.filter((s) => s.status !== 'paused')
|
||||||
|
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set window dimensions to match video dimensions or fill the screen
|
// Set window dimensions to match video dimensions or fill the screen
|
||||||
@@ -334,24 +358,24 @@ function setDimensions (dimensions) {
|
|||||||
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
|
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
|
||||||
|
|
||||||
// Limit window size to screen size
|
// Limit window size to screen size
|
||||||
var screenWidth = window.screen.width
|
const screenWidth = window.screen.width
|
||||||
var screenHeight = window.screen.height
|
const screenHeight = window.screen.height
|
||||||
var aspectRatio = dimensions.width / dimensions.height
|
const aspectRatio = dimensions.width / dimensions.height
|
||||||
var scaleFactor = Math.min(
|
const scaleFactor = Math.min(
|
||||||
Math.min(screenWidth / dimensions.width, 1),
|
Math.min(screenWidth / dimensions.width, 1),
|
||||||
Math.min(screenHeight / dimensions.height, 1)
|
Math.min(screenHeight / dimensions.height, 1)
|
||||||
)
|
)
|
||||||
var width = Math.max(
|
const width = Math.max(
|
||||||
Math.floor(dimensions.width * scaleFactor),
|
Math.floor(dimensions.width * scaleFactor),
|
||||||
config.WINDOW_MIN_WIDTH
|
config.WINDOW_MIN_WIDTH
|
||||||
)
|
)
|
||||||
var height = Math.max(
|
const height = Math.max(
|
||||||
Math.floor(dimensions.height * scaleFactor),
|
Math.floor(dimensions.height * scaleFactor),
|
||||||
config.WINDOW_MIN_HEIGHT
|
config.WINDOW_MIN_HEIGHT
|
||||||
)
|
)
|
||||||
|
|
||||||
ipcRenderer.send('setAspectRatio', aspectRatio)
|
ipcRenderer.send('setAspectRatio', aspectRatio)
|
||||||
ipcRenderer.send('setBounds', {x: null, y: null, width, height})
|
ipcRenderer.send('setBounds', {contentBounds: true, x: null, y: null, width, height})
|
||||||
state.playing.aspectRatio = aspectRatio
|
state.playing.aspectRatio = aspectRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,25 +384,25 @@ function setDimensions (dimensions) {
|
|||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
if (!Array.isArray(files)) files = [ files ]
|
if (!Array.isArray(files)) files = [ files ]
|
||||||
|
|
||||||
if (state.modal) {
|
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))
|
||||||
|
} else if (url === 'player' && allSubtitles) {
|
||||||
|
// Drop subtitles onto a playing video: add subtitles
|
||||||
|
controllers.subtitles.addSubtitles(files, true)
|
||||||
|
} else if (url === 'home') {
|
||||||
|
// Drop files onto home screen: show Create Torrent
|
||||||
state.modal = null
|
state.modal = null
|
||||||
}
|
controllers.torrentList.showCreateTorrent(files)
|
||||||
|
} else {
|
||||||
var subtitles = files.filter(controllers.subtitles.isSubtitle)
|
// Drop files onto any other screen: show error
|
||||||
|
return onError('Please go back to the torrent list before creating a new torrent.')
|
||||||
if (state.location.url() === 'home' || subtitles.length === 0) {
|
|
||||||
if (files.every(TorrentPlayer.isTorrent)) {
|
|
||||||
if (state.location.url() !== 'home') {
|
|
||||||
dispatch('backToList')
|
|
||||||
}
|
|
||||||
// All .torrent files? Add them.
|
|
||||||
files.forEach((file) => controllers.torrentList.addTorrent(file))
|
|
||||||
} else {
|
|
||||||
// Show the Create Torrent screen. Let's seed those files.
|
|
||||||
controllers.torrentList.showCreateTorrent(files)
|
|
||||||
}
|
|
||||||
} else if (state.location.url() === 'player') {
|
|
||||||
controllers.subtitles.addSubtitles(subtitles, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update()
|
update()
|
||||||
@@ -398,7 +422,7 @@ function onError (err) {
|
|||||||
function onPaste (e) {
|
function onPaste (e) {
|
||||||
if (e.target.tagName.toLowerCase() === 'input') return
|
if (e.target.tagName.toLowerCase() === 'input') return
|
||||||
|
|
||||||
var torrentIds = electron.clipboard.readText().split('\n')
|
const torrentIds = electron.clipboard.readText().split('\n')
|
||||||
torrentIds.forEach(function (torrentId) {
|
torrentIds.forEach(function (torrentId) {
|
||||||
torrentId = torrentId.trim()
|
torrentId = torrentId.trim()
|
||||||
if (torrentId.length === 0) return
|
if (torrentId.length === 0) return
|
||||||
@@ -432,3 +456,14 @@ function onFullscreenChanged (e, isFullScreen) {
|
|||||||
|
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkDownloadPath () {
|
||||||
|
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
|
||||||
|
if (err) {
|
||||||
|
state.downloadPathStatus = 'missing'
|
||||||
|
return console.error(err)
|
||||||
|
}
|
||||||
|
if (stat.isDirectory()) state.downloadPathStatus = 'ok'
|
||||||
|
else state.downloadPathStatus = 'missing'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
116
src/renderer/pages/app.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
const colors = require('material-ui/styles/colors')
|
||||||
|
const React = require('react')
|
||||||
|
|
||||||
|
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
|
||||||
|
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
|
||||||
|
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
|
||||||
|
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
|
||||||
|
|
||||||
|
const Header = require('../components/header')
|
||||||
|
|
||||||
|
const Views = {
|
||||||
|
'home': require('./torrent-list-page'),
|
||||||
|
'player': require('./player-page'),
|
||||||
|
'create-torrent': require('./create-torrent-page'),
|
||||||
|
'preferences': require('./preferences-page')
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modals = {
|
||||||
|
'open-torrent-address-modal': require('../components/open-torrent-address-modal'),
|
||||||
|
'remove-torrent-modal': require('../components/remove-torrent-modal'),
|
||||||
|
'update-available-modal': require('../components/update-available-modal'),
|
||||||
|
'unsupported-media-modal': require('../components/unsupported-media-modal')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontFamily = process.platform === 'win32'
|
||||||
|
? '"Segoe UI", sans-serif'
|
||||||
|
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
|
||||||
|
lightBaseTheme.fontFamily = fontFamily
|
||||||
|
darkBaseTheme.fontFamily = fontFamily
|
||||||
|
darkBaseTheme.palette.primary1Color = colors.cyan500
|
||||||
|
darkBaseTheme.palette.primary2Color = colors.cyan500
|
||||||
|
darkBaseTheme.palette.primary3Color = colors.grey600
|
||||||
|
darkBaseTheme.palette.accent1Color = colors.redA200
|
||||||
|
darkBaseTheme.palette.accent2Color = colors.redA400
|
||||||
|
darkBaseTheme.palette.accent3Color = colors.redA100
|
||||||
|
|
||||||
|
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.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
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
const vdom = (
|
||||||
|
<MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}>
|
||||||
|
<div className={'app ' + cls.join(' ')}>
|
||||||
|
<Header state={state} />
|
||||||
|
{this.getErrorPopover()}
|
||||||
|
<div key='content' className='content'>{this.getView()}</div>
|
||||||
|
{this.getModal()}
|
||||||
|
</div>
|
||||||
|
</MuiThemeProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
return vdom
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
const ModalContents = Modals[state.modal.id]
|
||||||
|
return (
|
||||||
|
<MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}>
|
||||||
|
<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'
|
||||||
|
label='Cancel'
|
||||||
|
style={{
|
||||||
|
marginRight: 10
|
||||||
|
}}
|
||||||
|
onClick={dispatcher('cancel')} />
|
||||||
|
<RaisedButton
|
||||||
|
className='control'
|
||||||
|
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,8 +2,10 @@ const React = require('react')
|
|||||||
const Bitfield = require('bitfield')
|
const Bitfield = require('bitfield')
|
||||||
const prettyBytes = require('prettier-bytes')
|
const prettyBytes = require('prettier-bytes')
|
||||||
const zeroFill = require('zero-fill')
|
const zeroFill = require('zero-fill')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const Playlist = require('../lib/playlist')
|
||||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||||
@@ -11,15 +13,16 @@ module.exports = class Player extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
// Show the video as large as will fit in the window, play immediately
|
// 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
|
// If the video is on Chromecast or Airplay, show a title screen instead
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
var showVideo = state.playing.location === 'local'
|
const showVideo = state.playing.location === 'local'
|
||||||
|
const showControls = state.playing.location !== 'external'
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='player'
|
className='player'
|
||||||
onWheel={handleVolumeWheel}
|
onWheel={handleVolumeWheel}
|
||||||
onMouseMove={dispatcher('mediaMouseMoved')}>
|
onMouseMove={dispatcher('mediaMouseMoved')}>
|
||||||
{showVideo ? renderMedia(state) : renderCastScreen(state)}
|
{showVideo ? renderMedia(state) : renderCastScreen(state)}
|
||||||
{renderPlayerControls(state)}
|
{showControls ? renderPlayerControls(state) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -36,7 +39,7 @@ function renderMedia (state) {
|
|||||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||||
// Instead, grab the DOM node and play/pause it if necessary
|
// Instead, grab the DOM node and play/pause it if necessary
|
||||||
// Get the <video> or <audio> tag
|
// Get the <video> or <audio> tag
|
||||||
var mediaElement = document.querySelector(state.playing.type)
|
const mediaElement = document.querySelector(state.playing.type)
|
||||||
if (mediaElement !== null) {
|
if (mediaElement !== null) {
|
||||||
if (state.playing.isPaused && !mediaElement.paused) {
|
if (state.playing.isPaused && !mediaElement.paused) {
|
||||||
mediaElement.pause()
|
mediaElement.pause()
|
||||||
@@ -64,20 +67,20 @@ function renderMedia (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Switch to the newly added subtitle track, if available
|
// Switch to the newly added subtitle track, if available
|
||||||
var tracks = mediaElement.textTracks || []
|
const tracks = mediaElement.textTracks || []
|
||||||
for (var j = 0; j < tracks.length; j++) {
|
for (let j = 0; j < tracks.length; j++) {
|
||||||
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
const isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
||||||
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
|
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save video position
|
// Save video position
|
||||||
var file = state.getPlayingFileSummary()
|
const file = state.getPlayingFileSummary()
|
||||||
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
||||||
file.duration = state.playing.duration = mediaElement.duration
|
file.duration = state.playing.duration = mediaElement.duration
|
||||||
|
|
||||||
// Save selected subtitle
|
// Save selected subtitle
|
||||||
if (state.playing.subtitles.selectedIndex !== -1) {
|
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
|
file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath
|
||||||
} else if (file.selectedSubtitle != null) {
|
} else if (file.selectedSubtitle != null) {
|
||||||
delete file.selectedSubtitle
|
delete file.selectedSubtitle
|
||||||
@@ -87,11 +90,11 @@ function renderMedia (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add subtitles to the <video> tag
|
// Add subtitles to the <video> tag
|
||||||
var trackTags = []
|
const trackTags = []
|
||||||
if (state.playing.subtitles.selectedIndex >= 0) {
|
if (state.playing.subtitles.selectedIndex >= 0) {
|
||||||
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
for (let i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||||
var track = state.playing.subtitles.tracks[i]
|
const track = state.playing.subtitles.tracks[i]
|
||||||
var isSelected = state.playing.subtitles.selectedIndex === i
|
const isSelected = state.playing.subtitles.selectedIndex === i
|
||||||
trackTags.push(
|
trackTags.push(
|
||||||
<track
|
<track
|
||||||
key={i}
|
key={i}
|
||||||
@@ -104,10 +107,10 @@ function renderMedia (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the <audio> or <video> tag
|
// Create the <audio> or <video> tag
|
||||||
var MediaTagName = state.playing.type
|
const MediaTagName = state.playing.type
|
||||||
var mediaTag = (
|
const mediaTag = (
|
||||||
<MediaTagName
|
<MediaTagName
|
||||||
src={state.server.localURL}
|
src={Playlist.getCurrentLocalURL(state)}
|
||||||
onDoubleClick={dispatcher('toggleFullScreen')}
|
onDoubleClick={dispatcher('toggleFullScreen')}
|
||||||
onLoadedMetadata={onLoadedMetadata}
|
onLoadedMetadata={onLoadedMetadata}
|
||||||
onEnded={onEnded}
|
onEnded={onEnded}
|
||||||
@@ -134,21 +137,25 @@ function renderMedia (state) {
|
|||||||
// As soon as we know the video dimensions, resize the window
|
// As soon as we know the video dimensions, resize the window
|
||||||
function onLoadedMetadata (e) {
|
function onLoadedMetadata (e) {
|
||||||
if (state.playing.type !== 'video') return
|
if (state.playing.type !== 'video') return
|
||||||
var video = e.target
|
const video = e.target
|
||||||
var dimensions = {
|
const dimensions = {
|
||||||
width: video.videoWidth,
|
width: video.videoWidth,
|
||||||
height: video.videoHeight
|
height: video.videoHeight
|
||||||
}
|
}
|
||||||
dispatch('setDimensions', dimensions)
|
dispatch('setDimensions', dimensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the video completes, pause the video instead of looping
|
|
||||||
function onEnded (e) {
|
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) {
|
function onCanPlay (e) {
|
||||||
var elem = e.target
|
const elem = e.target
|
||||||
if (state.playing.type === 'video' &&
|
if (state.playing.type === 'video' &&
|
||||||
elem.webkitVideoDecodedByteCount === 0) {
|
elem.webkitVideoDecodedByteCount === 0) {
|
||||||
dispatch('mediaError', 'Video codec unsupported')
|
dispatch('mediaError', 'Video codec unsupported')
|
||||||
@@ -162,15 +169,15 @@ function renderMedia (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderOverlay (state) {
|
function renderOverlay (state) {
|
||||||
var elems = []
|
const elems = []
|
||||||
var audioMetadataElem = renderAudioMetadata(state)
|
const audioMetadataElem = renderAudioMetadata(state)
|
||||||
var spinnerElem = renderLoadingSpinner(state)
|
const spinnerElem = renderLoadingSpinner(state)
|
||||||
if (audioMetadataElem) elems.push(audioMetadataElem)
|
if (audioMetadataElem) elems.push(audioMetadataElem)
|
||||||
if (spinnerElem) elems.push(spinnerElem)
|
if (spinnerElem) elems.push(spinnerElem)
|
||||||
|
|
||||||
// Video fills the window, centered with black bars if necessary
|
// Video fills the window, centered with black bars if necessary
|
||||||
// Audio gets a static poster image and a summary of the file metadata.
|
// Audio gets a static poster image and a summary of the file metadata.
|
||||||
var style
|
let style
|
||||||
if (state.playing.type === 'audio') {
|
if (state.playing.type === 'audio') {
|
||||||
style = { backgroundImage: cssBackgroundImagePoster(state) }
|
style = { backgroundImage: cssBackgroundImagePoster(state) }
|
||||||
} else if (elems.length !== 0) {
|
} else if (elems.length !== 0) {
|
||||||
@@ -188,27 +195,27 @@ function renderOverlay (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAudioMetadata (state) {
|
function renderAudioMetadata (state) {
|
||||||
var fileSummary = state.getPlayingFileSummary()
|
const fileSummary = state.getPlayingFileSummary()
|
||||||
if (!fileSummary.audioInfo) return
|
if (!fileSummary.audioInfo) return
|
||||||
var info = fileSummary.audioInfo
|
const info = fileSummary.audioInfo
|
||||||
|
|
||||||
// Get audio track info
|
// Get audio track info
|
||||||
var title = info.title
|
let title = info.title
|
||||||
if (!title) {
|
if (!title) {
|
||||||
title = fileSummary.name
|
title = fileSummary.name
|
||||||
}
|
}
|
||||||
var artist = info.artist && info.artist[0]
|
let artist = info.artist && info.artist[0]
|
||||||
var album = info.album
|
let album = info.album
|
||||||
if (album && info.year && !album.includes(info.year)) {
|
if (album && info.year && !album.includes(info.year)) {
|
||||||
album += ' (' + info.year + ')'
|
album += ' (' + info.year + ')'
|
||||||
}
|
}
|
||||||
var track
|
let track
|
||||||
if (info.track && info.track.no && info.track.of) {
|
if (info.track && info.track.no && info.track.of) {
|
||||||
track = info.track.no + ' of ' + info.track.of
|
track = info.track.no + ' of ' + info.track.of
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a small info box in the middle of the screen with title/album/etc
|
// Show a small info box in the middle of the screen with title/album/etc
|
||||||
var elems = []
|
const elems = []
|
||||||
if (artist) {
|
if (artist) {
|
||||||
elems.push((
|
elems.push((
|
||||||
<div key='artist' className='audio-artist'>
|
<div key='artist' className='audio-artist'>
|
||||||
@@ -232,7 +239,7 @@ function renderAudioMetadata (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Align the title with the other info, if available. Otherwise, center title
|
// Align the title with the other info, if available. Otherwise, center title
|
||||||
var emptyLabel = (<label></label>)
|
const emptyLabel = (<label />)
|
||||||
elems.unshift((
|
elems.unshift((
|
||||||
<div key='title' className='audio-title'>
|
<div key='title' className='audio-title'>
|
||||||
{elems.length ? emptyLabel : undefined}{title}
|
{elems.length ? emptyLabel : undefined}{title}
|
||||||
@@ -244,14 +251,14 @@ function renderAudioMetadata (state) {
|
|||||||
|
|
||||||
function renderLoadingSpinner (state) {
|
function renderLoadingSpinner (state) {
|
||||||
if (state.playing.isPaused) return
|
if (state.playing.isPaused) return
|
||||||
var isProbablyStalled = state.playing.isStalled ||
|
const isProbablyStalled = state.playing.isStalled ||
|
||||||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
||||||
if (!isProbablyStalled) return
|
if (!isProbablyStalled) return
|
||||||
|
|
||||||
var prog = state.getPlayingTorrentSummary().progress || {}
|
const prog = state.getPlayingTorrentSummary().progress || {}
|
||||||
var fileProgress = 0
|
let fileProgress = 0
|
||||||
if (prog.files) {
|
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)
|
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +275,7 @@ function renderLoadingSpinner (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderCastScreen (state) {
|
function renderCastScreen (state) {
|
||||||
var castIcon, castType, isCast
|
let castIcon, castType, isCast
|
||||||
if (state.playing.location.startsWith('chromecast')) {
|
if (state.playing.location.startsWith('chromecast')) {
|
||||||
castIcon = 'cast_connected'
|
castIcon = 'cast_connected'
|
||||||
castType = 'Chromecast'
|
castType = 'Chromecast'
|
||||||
@@ -281,9 +288,12 @@ function renderCastScreen (state) {
|
|||||||
castIcon = 'tv'
|
castIcon = 'tv'
|
||||||
castType = 'DLNA'
|
castType = 'DLNA'
|
||||||
isCast = true
|
isCast = true
|
||||||
} else if (state.playing.location === 'vlc') {
|
} else if (state.playing.location === 'external') {
|
||||||
|
// TODO: get the player name in a more reliable way
|
||||||
|
const playerPath = state.saved.prefs.externalPlayerPath
|
||||||
|
const playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
|
||||||
castIcon = 'tv'
|
castIcon = 'tv'
|
||||||
castType = 'VLC'
|
castType = playerName
|
||||||
isCast = false
|
isCast = false
|
||||||
} else if (state.playing.location === 'error') {
|
} else if (state.playing.location === 'error') {
|
||||||
castIcon = 'error_outline'
|
castIcon = 'error_outline'
|
||||||
@@ -291,15 +301,15 @@ function renderCastScreen (state) {
|
|||||||
isCast = false
|
isCast = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var isStarting = state.playing.location.endsWith('-pending')
|
const isStarting = state.playing.location.endsWith('-pending')
|
||||||
var castName = state.playing.castName
|
const castName = state.playing.castName
|
||||||
var castStatus
|
let castStatus
|
||||||
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
|
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
|
||||||
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
|
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
|
||||||
else castStatus = ''
|
else castStatus = ''
|
||||||
|
|
||||||
// Show a nice title image, if possible
|
// Show a nice title image, if possible
|
||||||
var style = {
|
const style = {
|
||||||
backgroundImage: cssBackgroundImagePoster(state)
|
backgroundImage: cssBackgroundImagePoster(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,12 +327,12 @@ function renderCastScreen (state) {
|
|||||||
function renderCastOptions (state) {
|
function renderCastOptions (state) {
|
||||||
if (!state.devices.castMenu) return
|
if (!state.devices.castMenu) return
|
||||||
|
|
||||||
var {location, devices} = state.devices.castMenu
|
const {location, devices} = state.devices.castMenu
|
||||||
var player = state.devices[location]
|
const player = state.devices[location]
|
||||||
|
|
||||||
var items = devices.map(function (device, ix) {
|
const items = devices.map(function (device, ix) {
|
||||||
var isSelected = player.device === device
|
const isSelected = player.device === device
|
||||||
var name = device.name
|
const name = device.name
|
||||||
return (
|
return (
|
||||||
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
|
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
|
||||||
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||||
@@ -339,11 +349,11 @@ function renderCastOptions (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSubtitleOptions (state) {
|
function renderSubtitleOptions (state) {
|
||||||
var subtitles = state.playing.subtitles
|
const subtitles = state.playing.subtitles
|
||||||
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||||
|
|
||||||
var items = subtitles.tracks.map(function (track, ix) {
|
const items = subtitles.tracks.map(function (track, ix) {
|
||||||
var isSelected = state.playing.subtitles.selectedIndex === ix
|
const isSelected = state.playing.subtitles.selectedIndex === ix
|
||||||
return (
|
return (
|
||||||
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
|
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
|
||||||
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
||||||
@@ -352,8 +362,8 @@ function renderSubtitleOptions (state) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
const noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||||
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
const noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||||
return (
|
return (
|
||||||
<ul key='subtitle-options' className='options-list'>
|
<ul key='subtitle-options' className='options-list'>
|
||||||
{items}
|
{items}
|
||||||
@@ -366,32 +376,39 @@ function renderSubtitleOptions (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPlayerControls (state) {
|
function renderPlayerControls (state) {
|
||||||
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
const positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
const captionsClass = state.playing.subtitles.tracks.length === 0
|
||||||
? 'disabled'
|
? 'disabled'
|
||||||
: state.playing.subtitles.selectedIndex >= 0
|
: state.playing.subtitles.selectedIndex >= 0
|
||||||
? 'active'
|
? 'active'
|
||||||
: ''
|
: ''
|
||||||
|
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
|
||||||
|
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
|
||||||
|
|
||||||
var elements = [
|
const elements = [
|
||||||
<div key='playback-bar' className='playback-bar'>
|
<div key='playback-bar' className='playback-bar'>
|
||||||
{renderLoadingBar(state)}
|
{renderLoadingBar(state)}
|
||||||
<div
|
<div
|
||||||
key='cursor'
|
key='cursor'
|
||||||
className='playback-cursor'
|
className='playback-cursor'
|
||||||
style={playbackCursorStyle}>
|
style={playbackCursorStyle} />
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
key='scrub-bar'
|
key='scrub-bar'
|
||||||
className='scrub-bar'
|
className='scrub-bar'
|
||||||
draggable='true'
|
draggable='true'
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onClick={handleScrub}
|
onClick={handleScrub}
|
||||||
onDrag={handleScrub}>
|
onDrag={handleScrub} />
|
||||||
</div>
|
|
||||||
</div>,
|
</div>,
|
||||||
|
|
||||||
|
<i
|
||||||
|
key='skip-previous'
|
||||||
|
className={'icon skip-previous float-left ' + prevClass}
|
||||||
|
onClick={dispatcher('previousTrack')}>
|
||||||
|
skip_previous
|
||||||
|
</i>,
|
||||||
|
|
||||||
<i
|
<i
|
||||||
key='play'
|
key='play'
|
||||||
className='icon play-pause float-left'
|
className='icon play-pause float-left'
|
||||||
@@ -399,6 +416,13 @@ function renderPlayerControls (state) {
|
|||||||
{state.playing.isPaused ? 'play_arrow' : 'pause'}
|
{state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||||
</i>,
|
</i>,
|
||||||
|
|
||||||
|
<i
|
||||||
|
key='skip-next'
|
||||||
|
className={'icon skip-next float-left ' + nextClass}
|
||||||
|
onClick={dispatcher('nextTrack')}>
|
||||||
|
skip_next
|
||||||
|
</i>,
|
||||||
|
|
||||||
<i
|
<i
|
||||||
key='fullscreen'
|
key='fullscreen'
|
||||||
className='icon fullscreen float-right'
|
className='icon fullscreen float-right'
|
||||||
@@ -420,24 +444,24 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||||
var castTypes = ['chromecast', 'airplay', 'dlna']
|
const castTypes = ['chromecast', 'airplay', 'dlna']
|
||||||
var isCastingAnywhere = castTypes.some(
|
const isCastingAnywhere = castTypes.some(
|
||||||
(castType) => state.playing.location.startsWith(castType))
|
(castType) => state.playing.location.startsWith(castType))
|
||||||
|
|
||||||
// Add the cast buttons. Icons for each cast type, connected/disconnected:
|
// Add the cast buttons. Icons for each cast type, connected/disconnected:
|
||||||
var buttonIcons = {
|
const buttonIcons = {
|
||||||
'chromecast': {true: 'cast_connected', false: 'cast'},
|
'chromecast': {true: 'cast_connected', false: 'cast'},
|
||||||
'airplay': {true: 'airplay', false: 'airplay'},
|
'airplay': {true: 'airplay', false: 'airplay'},
|
||||||
'dlna': {true: 'tv', false: 'tv'}
|
'dlna': {true: 'tv', false: 'tv'}
|
||||||
}
|
}
|
||||||
castTypes.forEach(function (castType) {
|
castTypes.forEach(function (castType) {
|
||||||
// Do we show this button (eg. the Chromecast button) at all?
|
// Do we show this button (eg. the Chromecast button) at all?
|
||||||
var isCasting = state.playing.location.startsWith(castType)
|
const isCasting = state.playing.location.startsWith(castType)
|
||||||
var player = state.devices[castType]
|
const player = state.devices[castType]
|
||||||
if ((!player || player.getDevices().length === 0) && !isCasting) return
|
if ((!player || player.getDevices().length === 0) && !isCasting) return
|
||||||
|
|
||||||
// Show the button. Three options for eg the Chromecast button:
|
// Show the button. Three options for eg the Chromecast button:
|
||||||
var buttonClass, buttonHandler
|
let buttonClass, buttonHandler
|
||||||
if (isCasting) {
|
if (isCasting) {
|
||||||
// Option 1: we are currently connected to Chromecast. Button stops the cast.
|
// Option 1: we are currently connected to Chromecast. Button stops the cast.
|
||||||
buttonClass = 'active'
|
buttonClass = 'active'
|
||||||
@@ -451,7 +475,7 @@ function renderPlayerControls (state) {
|
|||||||
buttonClass = ''
|
buttonClass = ''
|
||||||
buttonHandler = dispatcher('toggleCastMenu', castType)
|
buttonHandler = dispatcher('toggleCastMenu', castType)
|
||||||
}
|
}
|
||||||
var buttonIcon = buttonIcons[castType][isCasting]
|
const buttonIcon = buttonIcons[castType][isCasting]
|
||||||
|
|
||||||
elements.push((
|
elements.push((
|
||||||
<i
|
<i
|
||||||
@@ -464,13 +488,13 @@ function renderPlayerControls (state) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Render volume slider
|
// Render volume slider
|
||||||
var volume = state.playing.volume
|
const volume = state.playing.volume
|
||||||
var volumeIcon = 'volume_' + (
|
const volumeIcon = 'volume_' + (
|
||||||
volume === 0 ? 'off'
|
volume === 0 ? 'off'
|
||||||
: volume < 0.3 ? 'mute'
|
: volume < 0.3 ? 'mute'
|
||||||
: volume < 0.6 ? 'down'
|
: volume < 0.6 ? 'down'
|
||||||
: 'up')
|
: 'up')
|
||||||
var volumeStyle = {
|
const volumeStyle = {
|
||||||
background: '-webkit-gradient(linear, left top, right top, ' +
|
background: '-webkit-gradient(linear, left top, right top, ' +
|
||||||
'color-stop(' + (volume * 100) + '%, #eee), ' +
|
'color-stop(' + (volume * 100) + '%, #eee), ' +
|
||||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
'color-stop(' + (volume * 100) + '%, #727272))'
|
||||||
@@ -490,14 +514,13 @@ function renderPlayerControls (state) {
|
|||||||
type='range' min='0' max='1' step='0.05'
|
type='range' min='0' max='1' step='0.05'
|
||||||
value={volume}
|
value={volume}
|
||||||
onChange={handleVolumeScrub}
|
onChange={handleVolumeScrub}
|
||||||
style={volumeStyle}
|
style={volumeStyle} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
||||||
// Show video playback progress
|
// Show video playback progress
|
||||||
var currentTimeStr = formatTime(state.playing.currentTime)
|
const currentTimeStr = formatTime(state.playing.currentTime)
|
||||||
var durationStr = formatTime(state.playing.duration)
|
const durationStr = formatTime(state.playing.duration)
|
||||||
elements.push((
|
elements.push((
|
||||||
<span key='time' className='time float-left'>
|
<span key='time' className='time float-left'>
|
||||||
{currentTimeStr} / {durationStr}
|
{currentTimeStr} / {durationStr}
|
||||||
@@ -524,7 +547,7 @@ function renderPlayerControls (state) {
|
|||||||
function handleDragStart (e) {
|
function handleDragStart (e) {
|
||||||
// Prevent the cursor from changing, eg to a green + icon on Mac
|
// Prevent the cursor from changing, eg to a green + icon on Mac
|
||||||
if (e.dataTransfer) {
|
if (e.dataTransfer) {
|
||||||
var dt = e.dataTransfer
|
const dt = e.dataTransfer
|
||||||
dt.effectAllowed = 'none'
|
dt.effectAllowed = 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,9 +556,9 @@ function renderPlayerControls (state) {
|
|||||||
function handleScrub (e) {
|
function handleScrub (e) {
|
||||||
if (!e.clientX) return
|
if (!e.clientX) return
|
||||||
dispatch('mediaMouseMoved')
|
dispatch('mediaMouseMoved')
|
||||||
var windowWidth = document.querySelector('body').clientWidth
|
const windowWidth = document.querySelector('body').clientWidth
|
||||||
var fraction = e.clientX / windowWidth
|
const fraction = e.clientX / windowWidth
|
||||||
var position = fraction * state.playing.duration /* seconds */
|
const position = fraction * state.playing.duration /* seconds */
|
||||||
dispatch('skipTo', position)
|
dispatch('skipTo', position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,18 +589,18 @@ function renderPlayerControls (state) {
|
|||||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||||
// can be 'spongey' / non-contiguous
|
// can be 'spongey' / non-contiguous
|
||||||
function renderLoadingBar (state) {
|
function renderLoadingBar (state) {
|
||||||
var torrentSummary = state.getPlayingTorrentSummary()
|
const torrentSummary = state.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary.progress) {
|
if (!torrentSummary.progress) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all contiguous parts of the torrent which are loaded
|
// Find all contiguous parts of the torrent which are loaded
|
||||||
var prog = torrentSummary.progress
|
const prog = torrentSummary.progress
|
||||||
var fileProg = prog.files[state.playing.fileIndex]
|
const fileProg = prog.files[state.playing.fileIndex]
|
||||||
var parts = []
|
const parts = []
|
||||||
var lastPiecePresent = false
|
let lastPiecePresent = false
|
||||||
for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
||||||
var partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
|
const partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
|
||||||
if (partPresent && !lastPiecePresent) {
|
if (partPresent && !lastPiecePresent) {
|
||||||
parts.push({start: i - fileProg.startPiece, count: 1})
|
parts.push({start: i - fileProg.startPiece, count: 1})
|
||||||
} else if (partPresent) {
|
} else if (partPresent) {
|
||||||
@@ -587,23 +610,23 @@ function renderLoadingBar (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Output some bars to show which parts of the file are loaded
|
// Output some bars to show which parts of the file are loaded
|
||||||
var loadingBarElems = parts.map(function (part, i) {
|
const loadingBarElems = parts.map(function (part, i) {
|
||||||
var style = {
|
const style = {
|
||||||
left: (100 * part.start / fileProg.numPieces) + '%',
|
left: (100 * part.start / fileProg.numPieces) + '%',
|
||||||
width: (100 * part.count / fileProg.numPieces) + '%'
|
width: (100 * part.count / fileProg.numPieces) + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div key={i} className='loading-bar-part' style={style}></div>)
|
return (<div key={i} className='loading-bar-part' style={style} />)
|
||||||
})
|
})
|
||||||
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
|
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the CSS background-image string for a poster image + dark vignette
|
// Returns the CSS background-image string for a poster image + dark vignette
|
||||||
function cssBackgroundImagePoster (state) {
|
function cssBackgroundImagePoster (state) {
|
||||||
var torrentSummary = state.getPlayingTorrentSummary()
|
const torrentSummary = state.getPlayingTorrentSummary()
|
||||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
if (!posterPath) return ''
|
if (!posterPath) return ''
|
||||||
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
return cssBackgroundImageDarkGradient() + `, url('${posterPath}')`
|
||||||
}
|
}
|
||||||
|
|
||||||
function cssBackgroundImageDarkGradient () {
|
function cssBackgroundImageDarkGradient () {
|
||||||
@@ -616,12 +639,12 @@ function formatTime (time) {
|
|||||||
return '0:00'
|
return '0:00'
|
||||||
}
|
}
|
||||||
|
|
||||||
var hours = Math.floor(time / 3600)
|
let hours = Math.floor(time / 3600)
|
||||||
var minutes = Math.floor(time % 3600 / 60)
|
let minutes = Math.floor(time % 3600 / 60)
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
minutes = zeroFill(2, minutes)
|
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
|
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
|
||||||
}
|
}
|
||||||
170
src/renderer/pages/preferences-page.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
const colors = require('material-ui/styles/colors')
|
||||||
|
const path = require('path')
|
||||||
|
const React = require('react')
|
||||||
|
|
||||||
|
const Checkbox = require('material-ui/Checkbox').default
|
||||||
|
const Heading = require('../components/heading')
|
||||||
|
const PathSelector = require('../components/path-selector')
|
||||||
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
|
||||||
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 playerName = path.basename(
|
||||||
|
this.props.state.unsaved.prefs.externalPlayerPath || 'VLC'
|
||||||
|
)
|
||||||
|
|
||||||
|
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={this.props.state.unsaved.prefs.externalPlayerPath} />
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleExternalPlayerPathChange (filePath) {
|
||||||
|
if (path.extname(filePath) === '.app') {
|
||||||
|
// Mac: Use executable in packaged .app bundle
|
||||||
|
filePath += '/Contents/MacOS/' + path.basename(filePath, '.app')
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
</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
|
||||||
@@ -7,43 +7,58 @@ const {dispatcher} = require('../lib/dispatcher')
|
|||||||
|
|
||||||
module.exports = class TorrentList extends React.Component {
|
module.exports = class TorrentList extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
var torrentRows = state.saved.torrents.map(
|
|
||||||
|
const contents = []
|
||||||
|
if (state.downloadPathStatus === 'missing') {
|
||||||
|
contents.push(
|
||||||
|
<div key='torrent-missing-path'>
|
||||||
|
<p>Download path missing: {state.saved.prefs.downloadPath}</p>
|
||||||
|
<p>Check that all drives are connected?</p>
|
||||||
|
<p>Alternatively, choose a new download path
|
||||||
|
in <a href='#' onClick={dispatcher('preferences')}>Preferences</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const torrentElems = state.saved.torrents.map(
|
||||||
(torrentSummary) => this.renderTorrent(torrentSummary)
|
(torrentSummary) => this.renderTorrent(torrentSummary)
|
||||||
)
|
)
|
||||||
|
contents.push(...torrentElems)
|
||||||
|
contents.push(
|
||||||
|
<div key='torrent-placeholder' className='torrent-placeholder'>
|
||||||
|
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='torrent-list' className='torrent-list'>
|
<div key='torrent-list' className='torrent-list'>
|
||||||
{torrentRows}
|
{contents}
|
||||||
<div key='torrent-placeholder' className='torrent-placeholder'>
|
|
||||||
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTorrent (torrentSummary) {
|
renderTorrent (torrentSummary) {
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
var infoHash = torrentSummary.infoHash
|
const infoHash = torrentSummary.infoHash
|
||||||
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
const isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||||
|
|
||||||
// Background image: show some nice visuals, like a frame from the movie, if possible
|
// Background image: show some nice visuals, like a frame from the movie, if possible
|
||||||
var style = {}
|
const style = {}
|
||||||
if (torrentSummary.posterFileName) {
|
if (torrentSummary.posterFileName) {
|
||||||
var gradient = isSelected
|
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
|
||||||
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
|
style.backgroundImage = `${gradient}, url('${posterPath}')`
|
||||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
|
||||||
style.backgroundImage = gradient + `, url('${posterPath}')`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Foreground: name of the torrent, basic info like size, play button,
|
// Foreground: name of the torrent, basic info like size, play button,
|
||||||
// cast buttons if available, and delete
|
// cast buttons if available, and delete
|
||||||
var classes = ['torrent']
|
const classes = ['torrent']
|
||||||
// playStatus turns the play button into a loading spinner or error icon
|
// playStatus turns the play button into a loading spinner or error icon
|
||||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
||||||
if (isSelected) classes.push('selected')
|
if (isSelected) classes.push('selected')
|
||||||
if (!infoHash) classes.push('disabled')
|
if (!infoHash) classes.push('disabled')
|
||||||
|
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={torrentSummary.torrentKey}
|
key={torrentSummary.torrentKey}
|
||||||
@@ -60,16 +75,23 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
|
|
||||||
// Show name, download status, % complete
|
// Show name, download status, % complete
|
||||||
renderTorrentMetadata (torrentSummary) {
|
renderTorrentMetadata (torrentSummary) {
|
||||||
var name = torrentSummary.name || 'Loading torrent...'
|
const name = torrentSummary.name || 'Loading torrent...'
|
||||||
var elements = [(
|
const elements = [(
|
||||||
<div key='name' className='name ellipsis'>{name}</div>
|
<div key='name' className='name ellipsis'>{name}</div>
|
||||||
)]
|
)]
|
||||||
|
|
||||||
// If it's downloading/seeding then show progress info
|
// If it's downloading/seeding then show progress info
|
||||||
var prog = torrentSummary.progress
|
const prog = torrentSummary.progress
|
||||||
if (torrentSummary.status !== 'paused' && prog) {
|
if (torrentSummary.error) {
|
||||||
elements.push((
|
elements.push(
|
||||||
<div key='progress-info' className='ellipsis'>
|
<div key='progress-info' className='ellipsis'>
|
||||||
|
{getErrorMessage(torrentSummary)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (torrentSummary.status !== 'paused' && prog) {
|
||||||
|
elements.push(
|
||||||
|
<div key='progress-info' className='ellipsis'>
|
||||||
|
{renderProgressBar()}
|
||||||
{renderPercentProgress()}
|
{renderPercentProgress()}
|
||||||
{renderTotalProgress()}
|
{renderTotalProgress()}
|
||||||
{renderPeers()}
|
{renderPeers()}
|
||||||
@@ -77,19 +99,24 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
{renderUploadSpeed()}
|
{renderUploadSpeed()}
|
||||||
{renderEta()}
|
{renderEta()}
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div key='metadata' className='metadata'>{elements}</div>)
|
return (<div key='metadata' className='metadata'>{elements}</div>)
|
||||||
|
|
||||||
|
function renderProgressBar () {
|
||||||
|
const progress = Math.floor(100 * prog.progress)
|
||||||
|
return (<progress value={progress} max='100'>{progress}%</progress>)
|
||||||
|
}
|
||||||
|
|
||||||
function renderPercentProgress () {
|
function renderPercentProgress () {
|
||||||
var progress = Math.floor(100 * prog.progress)
|
const progress = Math.floor(100 * prog.progress)
|
||||||
return (<span key='percent-progress'>{progress}%</span>)
|
return (<span key='percent-progress'>{progress}%</span>)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTotalProgress () {
|
function renderTotalProgress () {
|
||||||
var downloaded = prettyBytes(prog.downloaded)
|
const downloaded = prettyBytes(prog.downloaded)
|
||||||
var total = prettyBytes(prog.length || 0)
|
const total = prettyBytes(prog.length || 0)
|
||||||
if (downloaded === total) {
|
if (downloaded === total) {
|
||||||
return (<span key='total-progress'>{downloaded}</span>)
|
return (<span key='total-progress'>{downloaded}</span>)
|
||||||
} else {
|
} else {
|
||||||
@@ -99,7 +126,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
|
|
||||||
function renderPeers () {
|
function renderPeers () {
|
||||||
if (prog.numPeers === 0) return
|
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>)
|
return (<span key='peers'>{prog.numPeers} {count}</span>)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,22 +141,22 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderEta () {
|
function renderEta () {
|
||||||
var downloaded = prog.downloaded
|
const downloaded = prog.downloaded
|
||||||
var total = prog.length || 0
|
const total = prog.length || 0
|
||||||
var missing = total - downloaded
|
const missing = total - downloaded
|
||||||
var downloadSpeed = prog.downloadSpeed
|
const downloadSpeed = prog.downloadSpeed
|
||||||
if (downloadSpeed === 0 || missing === 0) return
|
if (downloadSpeed === 0 || missing === 0) return
|
||||||
|
|
||||||
var rawEta = missing / downloadSpeed
|
const rawEta = missing / downloadSpeed
|
||||||
var hours = Math.floor(rawEta / 3600) % 24
|
const hours = Math.floor(rawEta / 3600) % 24
|
||||||
var minutes = Math.floor(rawEta / 60) % 60
|
const minutes = Math.floor(rawEta / 60) % 60
|
||||||
var seconds = Math.floor(rawEta % 60)
|
const seconds = Math.floor(rawEta % 60)
|
||||||
|
|
||||||
// Only display hours and minutes if they are greater than 0 but always
|
// Only display hours and minutes if they are greater than 0 but always
|
||||||
// display minutes if hours is being displayed
|
// display minutes if hours is being displayed
|
||||||
var hoursStr = hours ? hours + 'h' : ''
|
const hoursStr = hours ? hours + 'h' : ''
|
||||||
var minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
||||||
var secondsStr = seconds + 's'
|
const secondsStr = seconds + 's'
|
||||||
|
|
||||||
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
|
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
|
||||||
}
|
}
|
||||||
@@ -138,9 +165,9 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
// Download button toggles between torrenting (DL/seed) and paused
|
// Download button toggles between torrenting (DL/seed) and paused
|
||||||
// Play button starts streaming the torrent immediately, unpausing if needed
|
// Play button starts streaming the torrent immediately, unpausing if needed
|
||||||
renderTorrentButtons (torrentSummary) {
|
renderTorrentButtons (torrentSummary) {
|
||||||
var infoHash = torrentSummary.infoHash
|
const infoHash = torrentSummary.infoHash
|
||||||
|
|
||||||
var playIcon, playTooltip, playClass
|
let playIcon, playTooltip, playClass
|
||||||
if (torrentSummary.playStatus === 'timeout') {
|
if (torrentSummary.playStatus === 'timeout') {
|
||||||
playIcon = 'warning'
|
playIcon = 'warning'
|
||||||
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
||||||
@@ -149,7 +176,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
playTooltip = 'Start streaming'
|
playTooltip = 'Start streaming'
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadIcon, downloadTooltip
|
let downloadIcon, downloadTooltip
|
||||||
if (torrentSummary.status === 'seeding') {
|
if (torrentSummary.status === 'seeding') {
|
||||||
downloadIcon = 'file_upload'
|
downloadIcon = 'file_upload'
|
||||||
downloadTooltip = 'Seeding. Click to stop.'
|
downloadTooltip = 'Seeding. Click to stop.'
|
||||||
@@ -161,36 +188,10 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
downloadTooltip = 'Click to start torrenting.'
|
downloadTooltip = 'Click to start torrenting.'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do we have a saved position? Show it using a radial progress bar on top
|
// Only show the play/dowload buttons for torrents that contain playable media
|
||||||
// of the play button, unless already showing a spinner there:
|
let playButton, downloadButton, positionElem
|
||||||
var positionElem
|
if (!torrentSummary.error) {
|
||||||
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
downloadButton = (
|
||||||
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
|
|
||||||
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
|
||||||
playButton = (
|
|
||||||
<i
|
|
||||||
key='play-button'
|
|
||||||
title={playTooltip}
|
|
||||||
className={'button-round icon play ' + playClass}
|
|
||||||
onClick={dispatcher('playFile', infoHash)}>
|
|
||||||
{playIcon}
|
|
||||||
</i>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key='buttons' className='buttons'>
|
|
||||||
{positionElem}
|
|
||||||
{playButton}
|
|
||||||
<i
|
<i
|
||||||
key='download-button'
|
key='download-button'
|
||||||
className={'button-round icon download ' + torrentSummary.status}
|
className={'button-round icon download ' + torrentSummary.status}
|
||||||
@@ -198,6 +199,37 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
onClick={dispatcher('toggleTorrent', infoHash)}>
|
onClick={dispatcher('toggleTorrent', infoHash)}>
|
||||||
{downloadIcon}
|
{downloadIcon}
|
||||||
</i>
|
</i>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
const willShowSpinner = torrentSummary.playStatus === 'requested'
|
||||||
|
const mostRecentFile = torrentSummary.files &&
|
||||||
|
torrentSummary.files[torrentSummary.mostRecentFileIndex]
|
||||||
|
if (mostRecentFile && mostRecentFile.currentTime && !willShowSpinner) {
|
||||||
|
const fraction = mostRecentFile.currentTime / mostRecentFile.duration
|
||||||
|
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
|
||||||
|
playClass = 'resume-position'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||||
|
playButton = (
|
||||||
|
<i
|
||||||
|
key='play-button'
|
||||||
|
title={playTooltip}
|
||||||
|
className={'button-round icon play ' + playClass}
|
||||||
|
onClick={dispatcher('playFile', infoHash)}>
|
||||||
|
{playIcon}
|
||||||
|
</i>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key='buttons' className='buttons'>
|
||||||
|
{positionElem}
|
||||||
|
{playButton}
|
||||||
|
{downloadButton}
|
||||||
<i
|
<i
|
||||||
key='delete-button'
|
key='delete-button'
|
||||||
className='icon delete'
|
className='icon delete'
|
||||||
@@ -211,23 +243,32 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
|
|
||||||
// Show files, per-file download status and play buttons, and so on
|
// Show files, per-file download status and play buttons, and so on
|
||||||
renderTorrentDetails (torrentSummary) {
|
renderTorrentDetails (torrentSummary) {
|
||||||
var filesElement
|
let filesElement
|
||||||
if (!torrentSummary.files) {
|
if (torrentSummary.error || !torrentSummary.files) {
|
||||||
// We don't know what files this torrent contains
|
let message = ''
|
||||||
var message = torrentSummary.status === 'paused'
|
if (torrentSummary.error === 'path-missing') {
|
||||||
? 'Failed to load torrent info. Click the download button to try again...'
|
// Special case error: this torrent's download dir or file is missing
|
||||||
: 'Downloading torrent info...'
|
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
filesElement = (<div key='files' className='files warning'>{message}</div>)
|
} else if (torrentSummary.error) {
|
||||||
|
// General error for this torrent: just show the message
|
||||||
|
message = torrentSummary.error.message || torrentSummary.error
|
||||||
|
} else if (torrentSummary.status === 'paused') {
|
||||||
|
// No file info, no infohash, and we're not trying to download from the DHT
|
||||||
|
message = 'Failed to load torrent info. Click the download button to try again...'
|
||||||
|
} else {
|
||||||
|
// No file info, no infohash, trying to load from the DHT
|
||||||
|
message = 'Downloading torrent info...'
|
||||||
|
}
|
||||||
|
filesElement = (
|
||||||
|
<div key='files' className='files warning'>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// We do know the files. List them and show download stats for each one
|
// We do know the files. List them and show download stats for each one
|
||||||
var fileRows = torrentSummary.files
|
const fileRows = torrentSummary.files
|
||||||
.filter((file) => !file.path.includes('/.____padding_file/'))
|
.filter((file) => !file.path.includes('/.____padding_file/'))
|
||||||
.map((file, index) => ({ file, index }))
|
.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))
|
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
|
||||||
|
|
||||||
filesElement = (
|
filesElement = (
|
||||||
@@ -252,36 +293,39 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
renderFileRow (torrentSummary, file, index) {
|
renderFileRow (torrentSummary, file, index) {
|
||||||
// First, find out how much of the file we've downloaded
|
// First, find out how much of the file we've downloaded
|
||||||
// Are we even torrenting it?
|
// Are we even torrenting it?
|
||||||
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
const isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||||
var isDone = false // Are we finished torrenting it?
|
let isDone = false // Are we finished torrenting it?
|
||||||
var progress = ''
|
let progress = ''
|
||||||
if (torrentSummary.progress && torrentSummary.progress.files &&
|
if (torrentSummary.progress && torrentSummary.progress.files &&
|
||||||
torrentSummary.progress.files[index]) {
|
torrentSummary.progress.files[index]) {
|
||||||
var fileProg = torrentSummary.progress.files[index]
|
const fileProg = torrentSummary.progress.files[index]
|
||||||
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
||||||
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second, for media files where we saved our position, show how far we got
|
// Second, for media files where we saved our position, show how far we got
|
||||||
var positionElem
|
let positionElem
|
||||||
if (file.currentTime) {
|
if (file.currentTime) {
|
||||||
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
|
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
|
||||||
positionElem = this.renderRadialProgressBar(file.currentTime / file.duration)
|
positionElem = this.renderRadialProgressBar(file.currentTime / file.duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, render the file as a table row
|
// Finally, render the file as a table row
|
||||||
var isPlayable = TorrentPlayer.isPlayable(file)
|
const isPlayable = TorrentPlayer.isPlayable(file)
|
||||||
var infoHash = torrentSummary.infoHash
|
const infoHash = torrentSummary.infoHash
|
||||||
var icon
|
let icon
|
||||||
var handleClick
|
let handleClick
|
||||||
if (isPlayable) {
|
if (isPlayable) {
|
||||||
icon = 'play_arrow' /* playable? add option to play */
|
icon = 'play_arrow' /* playable? add option to play */
|
||||||
handleClick = dispatcher('playFile', infoHash, index)
|
handleClick = dispatcher('playFile', infoHash, index)
|
||||||
} else {
|
} else {
|
||||||
icon = 'description' /* file icon, opens in OS default app */
|
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 (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||||
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||||
return (
|
return (
|
||||||
@@ -308,23 +352,36 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderRadialProgressBar (fraction, cssClass) {
|
renderRadialProgressBar (fraction, cssClass) {
|
||||||
var rotation = 360 * fraction
|
const rotation = 360 * fraction
|
||||||
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
const transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
||||||
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
const transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='radial-progress' className={'radial-progress ' + cssClass}>
|
<div key='radial-progress' className={'radial-progress ' + cssClass}>
|
||||||
<div key='circle' className='circle'>
|
<div key='circle' className='circle'>
|
||||||
<div key='mask-full' className='mask full' style={transformFill}>
|
<div key='mask-full' className='mask full' style={transformFill}>
|
||||||
<div key='fill' className='fill' style={transformFill}></div>
|
<div key='fill' className='fill' style={transformFill} />
|
||||||
</div>
|
</div>
|
||||||
<div key='mask-half' className='mask half'>
|
<div key='mask-half' className='mask half'>
|
||||||
<div key='fill' className='fill' style={transformFill}></div>
|
<div key='fill' className='fill' style={transformFill} />
|
||||||
<div key='fill-fix' className='fill fix' style={transformFix}></div>
|
<div key='fill-fix' className='fill fix' style={transformFix} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div key='inset' className='inset'></div>
|
<div key='inset' className='inset' />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getErrorMessage (torrentSummary) {
|
||||||
|
const err = torrentSummary.error
|
||||||
|
if (err === 'path-missing') {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Path missing.<br />
|
||||||
|
Fix and restart the app, or delete the torrent.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return 'Error'
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
<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'></textarea>
|
|
||||||
</div>
|
|
||||||
<div key='trackers' className='torrent-attribute'>
|
|
||||||
<label>Trackers:</label>
|
|
||||||
<textarea className='torrent-attribute torrent-trackers' value={trackers}></textarea>
|
|
||||||
</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('back')}>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,105 +0,0 @@
|
|||||||
const React = require('react')
|
|
||||||
const remote = require('electron').remote
|
|
||||||
const dialog = remote.dialog
|
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
|
||||||
|
|
||||||
module.exports = class Preferences extends React.Component {
|
|
||||||
render () {
|
|
||||||
var state = this.props.state
|
|
||||||
return (
|
|
||||||
<div className='preferences'>
|
|
||||||
{renderGeneralSection(state)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGeneralSection (state) {
|
|
||||||
return renderSection({
|
|
||||||
key: 'general',
|
|
||||||
title: 'General',
|
|
||||||
description: '',
|
|
||||||
icon: 'settings'
|
|
||||||
}, [
|
|
||||||
renderDownloadDirSelector(state)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDownloadDirSelector (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) {
|
|
||||||
setStateValue('downloadPath', filePath)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renders a prefs section.
|
|
||||||
// - definition should be {icon, title, description}
|
|
||||||
// - controls should be an array of vdom elements
|
|
||||||
function renderSection (definition, controls) {
|
|
||||||
var helpElem = !definition.description ? null : (
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a file chooser
|
|
||||||
// - defition should be {label, description, options}
|
|
||||||
// options are passed to dialog.showOpenDialog
|
|
||||||
// - value should be the current pref, a file or folder path
|
|
||||||
// - callback takes a new file or folder path
|
|
||||||
function renderFileSelector (definition, value, callback) {
|
|
||||||
return (
|
|
||||||
<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'>
|
|
||||||
<input type='text' className='file-picker-text'
|
|
||||||
id={definition.property}
|
|
||||||
disabled='disabled'
|
|
||||||
value={value} />
|
|
||||||
<button className='btn' onClick={handleClick}>
|
|
||||||
<i className='icon'>folder_open</i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
function handleClick () {
|
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
|
||||||
if (!Array.isArray(filenames)) return
|
|
||||||
callback(filenames[0])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStateValue (property, value) {
|
|
||||||
dispatch('updatePreferences', property, value)
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
const React = require('react')
|
|
||||||
const electron = require('electron')
|
|
||||||
|
|
||||||
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 actionButton = state.modal.vlcInstalled
|
|
||||||
? (<button className='button-raised' onClick={dispatcher('vlcPlay')}>Play in VLC</button>)
|
|
||||||
: (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>)
|
|
||||||
var vlcMessage = state.modal.vlcNotFound
|
|
||||||
? 'Couldn\'t run VLC. 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'>{vlcMessage}</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.vlcInstalled = true // Assume they'll install it successfully
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,26 +2,26 @@
|
|||||||
// process from the main window.
|
// process from the main window.
|
||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
var deepEqual = require('deep-equal')
|
const crypto = require('crypto')
|
||||||
var defaultAnnounceList = require('create-torrent').announceList
|
const deepEqual = require('deep-equal')
|
||||||
var electron = require('electron')
|
const defaultAnnounceList = require('create-torrent').announceList
|
||||||
var fs = require('fs-extra')
|
const electron = require('electron')
|
||||||
var hat = require('hat')
|
const fs = require('fs-extra')
|
||||||
var musicmetadata = require('musicmetadata')
|
const musicmetadata = require('musicmetadata')
|
||||||
var networkAddress = require('network-address')
|
const networkAddress = require('network-address')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var WebTorrent = require('webtorrent')
|
const WebTorrent = require('webtorrent')
|
||||||
var zeroFill = require('zero-fill')
|
const zeroFill = require('zero-fill')
|
||||||
|
|
||||||
var crashReporter = require('../crash-reporter')
|
const crashReporter = require('../crash-reporter')
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var torrentPoster = require('./lib/torrent-poster')
|
const torrentPoster = require('./lib/torrent-poster')
|
||||||
|
|
||||||
// Report when the process crashes
|
// Report when the process crashes
|
||||||
crashReporter.init()
|
crashReporter.init()
|
||||||
|
|
||||||
// Send & receive messages from the main window
|
// Send & receive messages from the main window
|
||||||
var ipc = electron.ipcRenderer
|
const ipc = electron.ipcRenderer
|
||||||
|
|
||||||
// Force use of webtorrent trackers on all torrents
|
// Force use of webtorrent trackers on all torrents
|
||||||
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
||||||
@@ -31,7 +31,7 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
|||||||
/**
|
/**
|
||||||
* WebTorrent version.
|
* 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.
|
* Version number in Azureus-style. Generated from major and minor semver version.
|
||||||
@@ -39,7 +39,7 @@ var VERSION = require('../../package.json').version
|
|||||||
* '0.16.1' -> '0016'
|
* '0.16.1' -> '0016'
|
||||||
* '1.2.5' -> '0102'
|
* '1.2.5' -> '0102'
|
||||||
*/
|
*/
|
||||||
var VERSION_STR = VERSION.match(/([0-9]+)/g)
|
const VERSION_STR = VERSION.match(/([0-9]+)/g)
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map((v) => zeroFill(2, v))
|
.map((v) => zeroFill(2, v))
|
||||||
.join('')
|
.join('')
|
||||||
@@ -51,19 +51,19 @@ var VERSION_STR = VERSION.match(/([0-9]+)/g)
|
|||||||
* For example:
|
* For example:
|
||||||
* '-WW0102-'...
|
* '-WW0102-'...
|
||||||
*/
|
*/
|
||||||
var VERSION_PREFIX = '-WD' + VERSION_STR + '-'
|
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'
|
||||||
|
|
||||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||||
// client, as explained here: https://webtorrent.io/faq
|
// client, as explained here: https://webtorrent.io/faq
|
||||||
var client = window.client = new WebTorrent({
|
const client = window.client = new WebTorrent({
|
||||||
peerId: Buffer.from(VERSION_PREFIX + hat(48))
|
peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
|
||||||
})
|
})
|
||||||
|
|
||||||
// WebTorrent-to-HTTP streaming sever
|
// WebTorrent-to-HTTP streaming sever
|
||||||
var server = null
|
let server = null
|
||||||
|
|
||||||
// Used for diffing, so we only send progress updates when necessary
|
// Used for diffing, so we only send progress updates when necessary
|
||||||
var prevProgress = null
|
let prevProgress = null
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
@@ -83,8 +83,8 @@ function init () {
|
|||||||
generateTorrentPoster(torrentKey))
|
generateTorrentPoster(torrentKey))
|
||||||
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
||||||
getAudioMetadata(infoHash, index))
|
getAudioMetadata(infoHash, index))
|
||||||
ipc.on('wt-start-server', (e, infoHash, index) =>
|
ipc.on('wt-start-server', (e, infoHash) =>
|
||||||
startServer(infoHash, index))
|
startServer(infoHash))
|
||||||
ipc.on('wt-stop-server', (e) =>
|
ipc.on('wt-stop-server', (e) =>
|
||||||
stopServer())
|
stopServer())
|
||||||
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
||||||
@@ -97,14 +97,15 @@ function init () {
|
|||||||
true)
|
true)
|
||||||
|
|
||||||
setInterval(updateTorrentProgress, 1000)
|
setInterval(updateTorrentProgress, 1000)
|
||||||
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
|
||||||
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// Returns a WebTorrent object. See https://git.io/vik9M
|
||||||
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
||||||
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
||||||
|
|
||||||
var torrent = client.add(torrentID, {
|
const torrent = client.add(torrentID, {
|
||||||
path: path,
|
path: path,
|
||||||
fileModtimes: fileModtimes
|
fileModtimes: fileModtimes
|
||||||
})
|
})
|
||||||
@@ -120,15 +121,15 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopTorrenting (infoHash) {
|
function stopTorrenting (infoHash) {
|
||||||
var torrent = client.get(infoHash)
|
const torrent = client.get(infoHash)
|
||||||
if (torrent) torrent.destroy()
|
if (torrent) torrent.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new torrent, start seeding
|
// Create a new torrent, start seeding
|
||||||
function createTorrent (torrentKey, options) {
|
function createTorrent (torrentKey, options) {
|
||||||
console.log('creating torrent', torrentKey, options)
|
console.log('creating torrent', torrentKey, options)
|
||||||
var paths = options.files.map((f) => f.path)
|
const paths = options.files.map((f) => f.path)
|
||||||
var torrent = client.seed(paths, options)
|
const torrent = client.seed(paths, options)
|
||||||
torrent.key = torrentKey
|
torrent.key = torrentKey
|
||||||
addTorrentEvents(torrent)
|
addTorrentEvents(torrent)
|
||||||
ipc.send('wt-new-torrent')
|
ipc.send('wt-new-torrent')
|
||||||
@@ -146,14 +147,14 @@ function addTorrentEvents (torrent) {
|
|||||||
torrent.on('done', torrentDone)
|
torrent.on('done', torrentDone)
|
||||||
|
|
||||||
function torrentMetadata () {
|
function torrentMetadata () {
|
||||||
var info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipc.send('wt-metadata', torrent.key, info)
|
ipc.send('wt-metadata', torrent.key, info)
|
||||||
|
|
||||||
updateTorrentProgress()
|
updateTorrentProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentReady () {
|
function torrentReady () {
|
||||||
var info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipc.send('wt-ready', torrent.key, info)
|
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) // TODO: hack
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ function addTorrentEvents (torrent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function torrentDone () {
|
function torrentDone () {
|
||||||
var info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipc.send('wt-done', torrent.key, info)
|
ipc.send('wt-done', torrent.key, info)
|
||||||
|
|
||||||
updateTorrentProgress()
|
updateTorrentProgress()
|
||||||
@@ -197,9 +198,9 @@ function getTorrentFileInfo (file) {
|
|||||||
// Every time we resolve a magnet URI, save the torrent file so that we never
|
// 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.
|
// have to download it again. Never ask the DHT the same question twice.
|
||||||
function saveTorrentFile (torrentKey) {
|
function saveTorrentFile (torrentKey) {
|
||||||
var torrent = getTorrent(torrentKey)
|
const torrent = getTorrent(torrentKey)
|
||||||
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
|
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
|
||||||
var fileName = torrent.infoHash + '.torrent'
|
const fileName = torrent.infoHash + '.torrent'
|
||||||
if (exists) {
|
if (exists) {
|
||||||
// We've already saved the file
|
// We've already saved the file
|
||||||
return ipc.send('wt-file-saved', torrentKey, fileName)
|
return ipc.send('wt-file-saved', torrentKey, fileName)
|
||||||
@@ -219,7 +220,7 @@ function saveTorrentFile (torrentKey) {
|
|||||||
// Checks whether we've already resolved a given infohash to a torrent file
|
// 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
|
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
||||||
function checkIfTorrentFileExists (infoHash, cb) {
|
function checkIfTorrentFileExists (infoHash, cb) {
|
||||||
var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
const torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||||
fs.exists(torrentPath, function (exists) {
|
fs.exists(torrentPath, function (exists) {
|
||||||
cb(torrentPath, exists)
|
cb(torrentPath, exists)
|
||||||
})
|
})
|
||||||
@@ -228,14 +229,14 @@ function checkIfTorrentFileExists (infoHash, cb) {
|
|||||||
// Save a JPG that represents a torrent.
|
// Save a JPG that represents a torrent.
|
||||||
// Auto chooses either a frame from a video file, an image, etc
|
// Auto chooses either a frame from a video file, an image, etc
|
||||||
function generateTorrentPoster (torrentKey) {
|
function generateTorrentPoster (torrentKey) {
|
||||||
var torrent = getTorrent(torrentKey)
|
const torrent = getTorrent(torrentKey)
|
||||||
torrentPoster(torrent, function (err, buf, extension) {
|
torrentPoster(torrent, function (err, buf, extension) {
|
||||||
if (err) return console.log('error generating poster: %o', err)
|
if (err) return console.log('error generating poster: %o', err)
|
||||||
// save it for next time
|
// save it for next time
|
||||||
fs.mkdirp(config.POSTER_PATH, function (err) {
|
fs.mkdirp(config.POSTER_PATH, function (err) {
|
||||||
if (err) return console.log('error creating poster dir: %o', err)
|
if (err) return console.log('error creating poster dir: %o', err)
|
||||||
var posterFileName = torrent.infoHash + extension
|
const posterFileName = torrent.infoHash + extension
|
||||||
var posterFilePath = path.join(config.POSTER_PATH, posterFileName)
|
const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
|
||||||
fs.writeFile(posterFilePath, buf, function (err) {
|
fs.writeFile(posterFilePath, buf, function (err) {
|
||||||
if (err) return console.log('error saving poster: %o', err)
|
if (err) return console.log('error saving poster: %o', err)
|
||||||
// show the poster
|
// show the poster
|
||||||
@@ -246,7 +247,7 @@ function generateTorrentPoster (torrentKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateTorrentProgress () {
|
function updateTorrentProgress () {
|
||||||
var progress = getTorrentProgress()
|
const progress = getTorrentProgress()
|
||||||
// TODO: diff torrent-by-torrent, not once for the whole update
|
// TODO: diff torrent-by-torrent, not once for the whole update
|
||||||
if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
|
if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
|
||||||
return /* don't send heavy object if it hasn't changed */
|
return /* don't send heavy object if it hasn't changed */
|
||||||
@@ -257,19 +258,19 @@ function updateTorrentProgress () {
|
|||||||
|
|
||||||
function getTorrentProgress () {
|
function getTorrentProgress () {
|
||||||
// First, track overall progress
|
// First, track overall progress
|
||||||
var progress = client.progress
|
const progress = client.progress
|
||||||
var hasActiveTorrents = client.torrents.some(function (torrent) {
|
const hasActiveTorrents = client.torrents.some(function (torrent) {
|
||||||
return torrent.progress !== 1
|
return torrent.progress !== 1
|
||||||
})
|
})
|
||||||
|
|
||||||
// Track progress for every file in each torrent
|
// Track progress for every file in each torrent
|
||||||
// TODO: ideally this would be tracked by WebTorrent, which could do it
|
// TODO: ideally this would be tracked by WebTorrent, which could do it
|
||||||
// more efficiently than looping over torrent.bitfield
|
// more efficiently than looping over torrent.bitfield
|
||||||
var torrentProg = client.torrents.map(function (torrent) {
|
const torrentProg = client.torrents.map(function (torrent) {
|
||||||
var fileProg = torrent.files && torrent.files.map(function (file, index) {
|
const fileProg = torrent.files && torrent.files.map(function (file, index) {
|
||||||
var numPieces = file._endPiece - file._startPiece + 1
|
const numPieces = file._endPiece - file._startPiece + 1
|
||||||
var numPiecesPresent = 0
|
let numPiecesPresent = 0
|
||||||
for (var piece = file._startPiece; piece <= file._endPiece; piece++) {
|
for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
|
||||||
if (torrent.bitfield.get(piece)) numPiecesPresent++
|
if (torrent.bitfield.get(piece)) numPiecesPresent++
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -300,28 +301,28 @@ function getTorrentProgress () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServer (infoHash, index) {
|
function startServer (infoHash) {
|
||||||
var torrent = client.get(infoHash)
|
const torrent = client.get(infoHash)
|
||||||
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
|
if (torrent.ready) startServerFromReadyTorrent(torrent)
|
||||||
else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
|
else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServerFromReadyTorrent (torrent, index, cb) {
|
function startServerFromReadyTorrent (torrent, cb) {
|
||||||
if (server) return
|
if (server) return
|
||||||
|
|
||||||
// start the streaming torrent-to-http server
|
// start the streaming torrent-to-http server
|
||||||
server = torrent.createServer()
|
server = torrent.createServer()
|
||||||
server.listen(0, function () {
|
server.listen(0, function () {
|
||||||
var port = server.address().port
|
const port = server.address().port
|
||||||
var urlSuffix = ':' + port + '/' + index
|
const urlSuffix = ':' + port
|
||||||
var info = {
|
const info = {
|
||||||
torrentKey: torrent.key,
|
torrentKey: torrent.key,
|
||||||
localURL: 'http://localhost' + urlSuffix,
|
localURL: 'http://localhost' + urlSuffix,
|
||||||
networkURL: 'http://' + networkAddress() + urlSuffix
|
networkURL: 'http://' + networkAddress() + urlSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
ipc.send('wt-server-running', info)
|
ipc.send('wt-server-running', info)
|
||||||
ipc.send('wt-server-' + torrent.infoHash, info) // TODO: hack
|
ipc.send('wt-server-' + torrent.infoHash, info)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,8 +333,8 @@ function stopServer () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAudioMetadata (infoHash, index) {
|
function getAudioMetadata (infoHash, index) {
|
||||||
var torrent = client.get(infoHash)
|
const torrent = client.get(infoHash)
|
||||||
var file = torrent.files[index]
|
const file = torrent.files[index]
|
||||||
musicmetadata(file.createReadStream(), function (err, info) {
|
musicmetadata(file.createReadStream(), function (err, info) {
|
||||||
if (err) return
|
if (err) return
|
||||||
console.log('got audio metadata for %s: %o', file.name, info)
|
console.log('got audio metadata for %s: %o', file.name, info)
|
||||||
@@ -343,7 +344,7 @@ function getAudioMetadata (infoHash, index) {
|
|||||||
|
|
||||||
function selectFiles (torrentOrInfoHash, selections) {
|
function selectFiles (torrentOrInfoHash, selections) {
|
||||||
// Get the torrent object
|
// Get the torrent object
|
||||||
var torrent
|
let torrent
|
||||||
if (typeof torrentOrInfoHash === 'string') {
|
if (typeof torrentOrInfoHash === 'string') {
|
||||||
torrent = client.get(torrentOrInfoHash)
|
torrent = client.get(torrentOrInfoHash)
|
||||||
} else {
|
} else {
|
||||||
@@ -368,8 +369,8 @@ function selectFiles (torrentOrInfoHash, selections) {
|
|||||||
torrent.deselect(0, torrent.pieces.length - 1, false)
|
torrent.deselect(0, torrent.pieces.length - 1, false)
|
||||||
|
|
||||||
// Add selections (individual files)
|
// Add selections (individual files)
|
||||||
for (var i = 0; i < selections.length; i++) {
|
for (let i = 0; i < selections.length; i++) {
|
||||||
var file = torrent.files[i]
|
const file = torrent.files[i]
|
||||||
if (selections[i]) {
|
if (selections[i]) {
|
||||||
file.select()
|
file.select()
|
||||||
} else {
|
} else {
|
||||||
@@ -382,7 +383,7 @@ function selectFiles (torrentOrInfoHash, selections) {
|
|||||||
// Gets a WebTorrent handle by torrentKey
|
// Gets a WebTorrent handle by torrentKey
|
||||||
// Throws an Error if we're not currently torrenting anything w/ that key
|
// Throws an Error if we're not currently torrenting anything w/ that key
|
||||||
function getTorrent (torrentKey) {
|
function getTorrent (torrentKey) {
|
||||||
var ret = client.torrents.find((x) => x.key === torrentKey)
|
const ret = client.torrents.find((x) => x.key === torrentKey)
|
||||||
if (!ret) throw new Error('missing torrent key ' + torrentKey)
|
if (!ret) throw new Error('missing torrent key ' + torrentKey)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|||||||
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 |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
|
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 |
33
static/linux/share/applications/webtorrent-desktop.desktop
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=WebTorrent
|
||||||
|
Version=1.0
|
||||||
|
GenericName=BitTorrent Client
|
||||||
|
X-GNOME-FullName=WebTorrent
|
||||||
|
Comment=Download and share files over BitTorrent
|
||||||
|
Encoding=UTF-8
|
||||||
|
Type=Application
|
||||||
|
Icon=webtorrent-desktop
|
||||||
|
Terminal=false
|
||||||
|
Path=/opt/webtorrent-desktop
|
||||||
|
Exec=/opt/webtorrent-desktop/WebTorrent %U
|
||||||
|
TryExec=/opt/webtorrent-desktop/WebTorrent
|
||||||
|
StartupNotify=false
|
||||||
|
Categories=Network;FileTransfer;P2P;
|
||||||
|
MimeType=application/x-bittorrent;x-scheme-handler/magnet;x-scheme-handler/stream-magnet;
|
||||||
|
|
||||||
|
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
|
||||||
|
|
||||||
|
[Desktop Action CreateNewTorrent]
|
||||||
|
Name=Create New Torrent...
|
||||||
|
Exec=/opt/webtorrent-desktop/WebTorrent -n
|
||||||
|
Path=/opt/webtorrent-desktop
|
||||||
|
|
||||||
|
[Desktop Action OpenTorrentFile]
|
||||||
|
Name=Open Torrent File...
|
||||||
|
Exec=/opt/webtorrent-desktop/WebTorrent -o
|
||||||
|
Path=/opt/webtorrent-desktop
|
||||||
|
|
||||||
|
[Desktop Action OpenTorrentAddress]
|
||||||
|
Name=Open Torrent Address...
|
||||||
|
Exec=/opt/webtorrent-desktop/WebTorrent -u
|
||||||
|
Path=/opt/webtorrent-desktop
|
||||||
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
363
static/main.css
@@ -19,13 +19,17 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.app {
|
||||||
color: #FFF;
|
color: #FAFAFA; /* grey50 */
|
||||||
font-family: BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
|
font-family: BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app.is-win32 {
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
#body {
|
#body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -71,12 +75,12 @@ table {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
background: rgb(40, 40, 40);
|
background: rgb(30, 30, 30);
|
||||||
animation: fadein 0.5s;
|
animation: fadein 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app:not(.is-focused) {
|
.app:not(.is-focused) {
|
||||||
background: rgb(50, 50, 50);
|
background: rgb(40, 40, 40);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -87,9 +91,7 @@ table {
|
|||||||
font-family: 'Material Icons';
|
font-family: 'Material Icons';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Material Icons'),
|
src: url('MaterialIcons-Regular.woff2') format('woff2');
|
||||||
local('MaterialIcons-Regular'),
|
|
||||||
url(MaterialIcons-Regular.woff2) format('woff2');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@@ -136,33 +138,13 @@ table {
|
|||||||
float: right;
|
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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: rgb(40, 40, 40);
|
background: rgb(40, 40, 40);
|
||||||
border-bottom: 1px solid rgb(20, 20, 20);
|
border-bottom: 1px solid rgb(30, 30, 30);
|
||||||
height: 38px; /* vertically center OS menu buttons (OS X) */
|
height: 38px; /* vertically center OS menu buttons (OS X) */
|
||||||
padding-top: 7px;
|
padding-top: 7px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -239,7 +221,7 @@ table {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: overlay;
|
overflow-y: overlay;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
margin-top: 37px;
|
margin-top: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.view-player .content {
|
.app.view-player .content {
|
||||||
@@ -283,10 +265,6 @@ table {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.open-torrent-address-modal input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-torrent {
|
.create-torrent {
|
||||||
padding: 10px 25px;
|
padding: 10px 25px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -294,6 +272,7 @@ table {
|
|||||||
|
|
||||||
.create-torrent .torrent-attribute {
|
.create-torrent .torrent-attribute {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent .torrent-attribute>* {
|
.create-torrent .torrent-attribute>* {
|
||||||
@@ -301,13 +280,12 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent .torrent-attribute label {
|
.create-torrent .torrent-attribute label {
|
||||||
width: 60px;
|
width: 100px;
|
||||||
margin-right: 10px;
|
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent .torrent-attribute>div {
|
.create-torrent .torrent-attribute>div {
|
||||||
width: calc(100% - 90px);
|
width: calc(100% - 100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent .torrent-attribute div {
|
.create-torrent .torrent-attribute div {
|
||||||
@@ -316,29 +294,8 @@ table {
|
|||||||
text-overflow: ellipsis;
|
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
|
* BUTTONS
|
||||||
* See https://www.google.com/design/spec/components/buttons.html
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
a,
|
a,
|
||||||
@@ -361,62 +318,13 @@ i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
|
|||||||
text-align: center;
|
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,
|
||||||
input[type='text'] {
|
textarea,
|
||||||
background: transparent;
|
.control {
|
||||||
width: 300px;
|
font-size: 14px !important;
|
||||||
padding: 6px;
|
|
||||||
border: 1px solid #bbb;
|
|
||||||
border-radius: 3px;
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -430,7 +338,7 @@ input[type='text'] {
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
transition: -webkit-filter 0.1s ease-out;
|
transition: -webkit-filter 0.1s ease-out;
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: fadein .4s;
|
animation: fadein 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent,
|
.torrent,
|
||||||
@@ -442,10 +350,6 @@ input[type='text'] {
|
|||||||
border-bottom: 1px solid rgb(20, 20, 20);
|
border-bottom: 1px solid rgb(20, 20, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent:hover {
|
|
||||||
-webkit-filter: brightness(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent .metadata {
|
.torrent .metadata {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 25px;
|
top: 25px;
|
||||||
@@ -551,6 +455,33 @@ input[type='text'] {
|
|||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
width: 60px;
|
||||||
|
margin-right: 8px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-webkit-progress-bar {
|
||||||
|
background-color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-webkit-progress-value {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TORRENT LIST: ERRORS
|
||||||
|
*/
|
||||||
|
|
||||||
|
.torrent-list p {
|
||||||
|
margin: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-list a {
|
||||||
|
color: #99f;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TORRENT LIST: DRAG-DROP TARGET
|
* TORRENT LIST: DRAG-DROP TARGET
|
||||||
*/
|
*/
|
||||||
@@ -589,6 +520,7 @@ body.drag .app::after {
|
|||||||
/*
|
/*
|
||||||
* TORRENT LIST: EXPANDED TORRENT DETAILS
|
* TORRENT LIST: EXPANDED TORRENT DETAILS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.torrent.selected {
|
.torrent.selected {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
@@ -612,10 +544,6 @@ body.drag .app::after {
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details tr:hover {
|
|
||||||
background-color: rgba(200, 200, 200, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent-details td {
|
.torrent-details td {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -712,11 +640,15 @@ body.drag .app::after {
|
|||||||
opacity: 1;
|
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;
|
font-size: 28px;
|
||||||
margin-top: 5px;
|
margin: 5px;
|
||||||
margin-right: 10px;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls .volume-slider {
|
.player .controls .volume-slider {
|
||||||
@@ -900,173 +832,6 @@ video::-webkit-media-text-track-container {
|
|||||||
font-weight: bold;
|
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 .btn {
|
|
||||||
display: inline-block;
|
|
||||||
-webkit-appearance: button;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
border-color: #cccccc;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #9da5b4;
|
|
||||||
text-shadow: none;
|
|
||||||
border: 1px solid #181a1f;
|
|
||||||
background-color: #3d3d3d;
|
|
||||||
white-space: initial;
|
|
||||||
font-size: 0.889em;
|
|
||||||
line-height: 1;
|
|
||||||
padding: 0.5em 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .btn .icon {
|
|
||||||
margin: 0;
|
|
||||||
color: #a8a8a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .help .icon {
|
|
||||||
vertical-align: sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.preferences .preferences-panel .control-group + .control-group {
|
|
||||||
margin-top: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .section {
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid #181a1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .section:first {
|
|
||||||
border-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .section:first-child,
|
|
||||||
.preferences .section:last-child {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .section.section:empty {
|
|
||||||
padding: 0;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .section-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences section .section-heading,
|
|
||||||
.preferences .section .section-heading {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: #dcdcdc;
|
|
||||||
font-size: 1.75em;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 1;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .sub-section-heading.icon:before,
|
|
||||||
.preferences .section-heading.icon:before {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .section-heading-count {
|
|
||||||
margin-left: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .section-body {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .sub-section {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .sub-section .sub-section-heading {
|
|
||||||
color: #dcdcdc;
|
|
||||||
font-size: 1.4em;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 1;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .preferences-panel label {
|
|
||||||
color: #a8a8a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .preferences-panel .control-group + .control-group {
|
|
||||||
margin-top: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .preferences-panel .control-group .editor-container {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .preference-title {
|
|
||||||
font-size: 1.2em;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .preference-description {
|
|
||||||
color: rgba(170, 170, 170, 0.6);
|
|
||||||
-webkit-user-select: none;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences input {
|
|
||||||
font-size: 1.1em;
|
|
||||||
line-height: 1.15em;
|
|
||||||
max-height: none;
|
|
||||||
width: 100%;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #a8a8a8;
|
|
||||||
border: 1px solid #181a1f;
|
|
||||||
background-color: #1b1d23;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences input::-webkit-input-placeholder {
|
|
||||||
color: rgba(170, 170, 170, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .control-group input {
|
|
||||||
margin-top: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .control-group input.file-picker-text {
|
|
||||||
width: calc(100% - 40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferences .control-group .checkbox .icon {
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin: 0;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MEDIA OVERLAY / AUDIO DETAILS
|
* MEDIA OVERLAY / AUDIO DETAILS
|
||||||
*/
|
*/
|
||||||
@@ -1225,3 +990,19 @@ video::-webkit-media-text-track-container {
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
margin: 4px 0 0 4px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,11 +7,7 @@
|
|||||||
<link rel="stylesheet" href="main.css">
|
<link rel="stylesheet" href="main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- React prints a warning if you render to <body> directly -->
|
|
||||||
<div id='body'></div>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||