Compare commits
3 Commits
esm
...
feross-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aec43c73f2 | ||
|
|
3baa74d3f9 | ||
|
|
9b91b8c76a |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github: feross
|
||||||
|
custom: https://paypal.me/borewit
|
||||||
11
.github/ISSUE_TEMPLATE.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- DO NOT POST LINKS OR REFERENCES TO COPYRIGHTED CONTENT IN YOUR ISSUE. -->
|
||||||
|
|
||||||
|
**What version of WebTorrent Desktop?** (See the 'About WebTorrent' menu)
|
||||||
|
|
||||||
|
**What operating system and version?**
|
||||||
|
|
||||||
|
**What did you do?**
|
||||||
|
|
||||||
|
**What did you expect to happen?**
|
||||||
|
|
||||||
|
**What actually happened?**
|
||||||
20
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
20
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: "🐞 Bug report"
|
|
||||||
about: Report an issue with this software
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- DO NOT POST LINKS OR REFERENCES TO COPYRIGHTED CONTENT IN YOUR ISSUE. -->
|
|
||||||
|
|
||||||
**What version of WebTorrent Desktop are you using?**
|
|
||||||
|
|
||||||
**What operating system and version?**
|
|
||||||
|
|
||||||
**What happened?**
|
|
||||||
|
|
||||||
**What did you expect to happen?**
|
|
||||||
|
|
||||||
**Are you willing to submit a pull request to fix this bug?**
|
|
||||||
20
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
20
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: "⭐️ Feature request"
|
|
||||||
about: Request a new feature to be added
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- DO NOT POST LINKS OR REFERENCES TO COPYRIGHTED CONTENT IN YOUR ISSUE. -->
|
|
||||||
|
|
||||||
**What version of WebTorrent Desktop are you using?**
|
|
||||||
|
|
||||||
**What operating system and version?**
|
|
||||||
|
|
||||||
**What problem do you want to solve?**
|
|
||||||
|
|
||||||
**What do you think is the correct solution to this problem?**
|
|
||||||
|
|
||||||
**Are you willing to submit a pull request to implement this change?**
|
|
||||||
15
.github/config.yml
vendored
Normal file
15
.github/config.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# ProBot Request Info (https://probot.github.io/apps/request-info/)
|
||||||
|
|
||||||
|
requestInfoReplyComment: >
|
||||||
|
👋 We would appreciate it if you could provide us with more information about this
|
||||||
|
issue. <br><br> 
|
||||||
|
requestInfoLabelToAdd: 'need more info'
|
||||||
|
|
||||||
|
# ProBot Welcome (https://probot.github.io/apps/welcome/)
|
||||||
|
|
||||||
|
newPRWelcomeComment: >
|
||||||
|
🙌 Thanks for opening this pull request! You're awesome.
|
||||||
|
<br><br> 
|
||||||
|
firstPRMergeComment: >
|
||||||
|
🎉 Congrats on getting your first pull request landed! <br><br>
|
||||||
|

|
||||||
4
.github/lock.yml
vendored
Normal file
4
.github/lock.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# ProBot Lock (https://probot.github.io/apps/lock/)
|
||||||
|
|
||||||
|
daysUntilLock: 365
|
||||||
|
lockComment: false
|
||||||
9
.github/no-response.yml
vendored
Normal file
9
.github/no-response.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
||||||
|
|
||||||
|
daysUntilClose: 14
|
||||||
|
responseRequiredLabel: 'need more info'
|
||||||
|
closeComment: >
|
||||||
|
This issue has been automatically closed because there was no response to a
|
||||||
|
request for more information from the issue opener. Please leave a comment or
|
||||||
|
open a new issue if you have additional information related to this issue.
|
||||||
|
<br><br> 
|
||||||
17
.github/stale.yml
vendored
Normal file
17
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||||
|
|
||||||
|
daysUntilStale: 90
|
||||||
|
daysUntilClose: 14
|
||||||
|
exemptLabels:
|
||||||
|
- accepted
|
||||||
|
- blocked
|
||||||
|
- bug
|
||||||
|
- enhancement
|
||||||
|
- greenkeeper
|
||||||
|
- 'help wanted'
|
||||||
|
- meta
|
||||||
|
staleLabel: stale
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs.
|
||||||
|
closeComment: false
|
||||||
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: ci
|
|
||||||
on: [push,pull_request]
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Node ${{ matrix.node }} / ${{ matrix.os }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- ubuntu-latest
|
|
||||||
node:
|
|
||||||
- '16'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node }}
|
|
||||||
- run: npm install
|
|
||||||
- run: npm run build --if-present
|
|
||||||
- run: npm test
|
|
||||||
69
.github/workflows/package.yml
vendored
69
.github/workflows/package.yml
vendored
@@ -1,69 +0,0 @@
|
|||||||
name: package
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
|
||||||
package_linux:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: '16'
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
- run: npm install
|
|
||||||
- run: npm run package -- linux
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: linux
|
|
||||||
path: |
|
|
||||||
dist/*.deb
|
|
||||||
dist/*.rpm
|
|
||||||
dist/*.zip
|
|
||||||
package_macos:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: '16'
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
- run: npm install
|
|
||||||
- run: npm run package -- darwin
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: macos
|
|
||||||
path: |
|
|
||||||
dist/*.dmg
|
|
||||||
dist/*.zip
|
|
||||||
package_windows:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: '16'
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
- run: npm install
|
|
||||||
- run: npm run package -- win32
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: windows
|
|
||||||
path: |
|
|
||||||
dist/*.exe
|
|
||||||
dist/*.nupkg
|
|
||||||
dist/*.zip
|
|
||||||
23
.github/workflows/stale.yml
vendored
23
.github/workflows/stale.yml
vendored
@@ -1,23 +0,0 @@
|
|||||||
name: Mark stale issues and pull requests
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 12 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v4
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
stale-issue-message: 'Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?'
|
|
||||||
stale-pr-message: 'Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?'
|
|
||||||
exempt-issue-labels: accepted,blocked,bug,dependency,enhancement,'help wanted',question,security,meta
|
|
||||||
exempt-pr-labels: accepted,blocked,bug,dependency,enhancement,'help wanted',question,security,meta
|
|
||||||
stale-issue-label: 'stale'
|
|
||||||
stale-pr-label: 'stale'
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,7 +2,6 @@ node_modules/
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
.vscode/
|
|
||||||
|
|
||||||
# JetBrains IntelliJ IDEA project files
|
# JetBrains IntelliJ IDEA project files
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- lts/*
|
||||||
13
AUTHORS.md
13
AUTHORS.md
@@ -39,7 +39,6 @@
|
|||||||
- Nuno Campos (nuno.campos@me.com)
|
- Nuno Campos (nuno.campos@me.com)
|
||||||
- Ebrahim Byagowi (ebrahim@gnu.org)
|
- Ebrahim Byagowi (ebrahim@gnu.org)
|
||||||
- Josip Janzic (josip@jjanzic.com)
|
- Josip Janzic (josip@jjanzic.com)
|
||||||
- Egor Yurtaev (yurtaev.egor@gmail.com)
|
|
||||||
- Emil Bay (github@tixz.dk)
|
- Emil Bay (github@tixz.dk)
|
||||||
- Borewit (borewit@users.noreply.github.com)
|
- Borewit (borewit@users.noreply.github.com)
|
||||||
- greenkeeper[bot] (greenkeeper[bot]@users.noreply.github.com)
|
- greenkeeper[bot] (greenkeeper[bot]@users.noreply.github.com)
|
||||||
@@ -56,7 +55,6 @@
|
|||||||
- Dan Flettre (flettre@gmail.com)
|
- Dan Flettre (flettre@gmail.com)
|
||||||
- Sibiraj (dev.sibiraj@outlook.com)
|
- Sibiraj (dev.sibiraj@outlook.com)
|
||||||
- clujin (clujin@gmail.com)
|
- clujin (clujin@gmail.com)
|
||||||
- Sharon Grossman (sharong1337@gmail.com)
|
|
||||||
- Linus Unnebäck (linus@folkdatorn.se)
|
- Linus Unnebäck (linus@folkdatorn.se)
|
||||||
- Adrian Tombu (adrian@otso.fr)
|
- Adrian Tombu (adrian@otso.fr)
|
||||||
- Lucas (5874806+RecoX@users.noreply.github.com)
|
- Lucas (5874806+RecoX@users.noreply.github.com)
|
||||||
@@ -66,18 +64,7 @@
|
|||||||
- Recox (5874806+RecoX@users.noreply.github.com)
|
- Recox (5874806+RecoX@users.noreply.github.com)
|
||||||
- greenkeeper[bot] (23040076+greenkeeper[bot]@users.noreply.github.com)
|
- greenkeeper[bot] (23040076+greenkeeper[bot]@users.noreply.github.com)
|
||||||
- hicom150 (hicom150@gmail.com)
|
- hicom150 (hicom150@gmail.com)
|
||||||
- Дамјан Георгиевски (gdamjan@gmail.com)
|
|
||||||
- Jimmy Wärting (jimmy@warting.se)
|
- Jimmy Wärting (jimmy@warting.se)
|
||||||
- Julen Garcia Leunda (hicom150@gmail.com)
|
|
||||||
- Feross (feross@feross.org)
|
- Feross (feross@feross.org)
|
||||||
- Daniele Debernardi (drebrez@gmail.com)
|
|
||||||
- Chandan Chowdary Bhagam (chandandharana@gmail.com)
|
|
||||||
- Pieter Goetschalckx (3.14.e.ter@gmail.com)
|
|
||||||
- Carey Metcalfe (carey@cmetcalfe.ca)
|
|
||||||
- Ameet Kaustav (akaustav@users.noreply.github.com)
|
|
||||||
- gpatarin (gael.patarin@outlook.com)
|
|
||||||
- Gael Patarin (gael.patarin@outlook.com)
|
|
||||||
- Subin Siby (mail@subinsb.com)
|
|
||||||
- Hinara (hinara.turevel@gmail.com)
|
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,64 +1,6 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
## v0.24.0 - 2020-08-28
|
## v0.21.0 - 2019-08
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support the `.m2ts` video container format ([hicom150](https://github.com/hicom150))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Update to Electron 10.1.0 [\#1864](https://github.com/webtorrent/webtorrent-desktop/pull/1864) ([feross](https://github.com/feross))
|
|
||||||
- Update the Windows installer loading image [\#1841](https://github.com/webtorrent/webtorrent-desktop/pull/1841) ([alxhotel](https://github.com/alxhotel))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix music metadata not showing up [\#1847](https://github.com/webtorrent/webtorrent-desktop/pull/1847) ([Borewit](https://github.com/Borewit))
|
|
||||||
- Fix the "Play in VLC" functionality [\#1850](https://github.com/webtorrent/webtorrent-desktop/pull/1850) ([Hinara](https://github.com/Hinara))
|
|
||||||
- Prevent shortcuts from activating when user input elements are focused [\#1840](https://github.com/webtorrent/webtorrent-desktop/pull/1840) ([subins2000](https://github.com/subins2000))
|
|
||||||
|
|
||||||
## v0.23.0 - 2020-07-15
|
|
||||||
|
|
||||||
🔒 This release contains a critical security fix. Please update as soon as possible. [\#1837](https://github.com/webtorrent/webtorrent-desktop/issues/1837#issuecomment-729320901)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add macOS Notarization [\#1834](https://github.com/webtorrent/webtorrent-desktop/pull/1834) ([feross](https://github.com/feross))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Update to Electron 10 beta [\#1834](https://github.com/webtorrent/webtorrent-desktop/pull/1834) ([feross](https://github.com/feross))
|
|
||||||
|
|
||||||
## v0.22.0 - 2020-07-15
|
|
||||||
|
|
||||||
❤️✨ A new version of WebTorrent Desktop is out! ❤️✨
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Linux `.rpm` packages and `arm64` builds are now available! [\#1694](https://github.com/webtorrent/webtorrent-desktop/pull/1694) ([hicom150](https://github.com/hicom150))
|
|
||||||
- Add support for multiple audio tracks [\#1712](https://github.com/webtorrent/webtorrent-desktop/pull/1712) ([hicom150](https://github.com/hicom150))
|
|
||||||
- Improve codec unsupported detection [\#1711](https://github.com/webtorrent/webtorrent-desktop/pull/1711) ([hicom150](https://github.com/hicom150))
|
|
||||||
- Report when files are being verified [\#1717](https://github.com/webtorrent/webtorrent-desktop/pull/1717) ([pR0Ps](https://github.com/pR0Ps))
|
|
||||||
- Support additional audio files: MPEG-Layer-2, Musepack, Matroska audio, WavePack [\#1772](https://github.com/webtorrent/webtorrent-desktop/pull/1772)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Update to Electron 9 [\#1729](https://github.com/webtorrent/webtorrent-desktop/pull/1729) [\#1832](https://github.com/webtorrent/webtorrent-desktop/issues/1832)
|
|
||||||
- Update to music-metadata 4.8.0 [\#1719](https://github.com/webtorrent/webtorrent-desktop/pull/1719) ([Borewit](https://github.com/Borewit))
|
|
||||||
- Update Windows build documentation [\#1715](https://github.com/webtorrent/webtorrent-desktop/pull/1715) ([RecoX](https://github.com/RecoX))
|
|
||||||
- Remove unneeded dependencies ([feross](https://github.com/feross))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix a few type errors [\#1720](https://github.com/webtorrent/webtorrent-desktop/pull/1720) ([mathiasvr](https://github.com/mathiasvr))
|
|
||||||
- Fix electron SUID sandbox error [\#1707](https://github.com/webtorrent/webtorrent-desktop/pull/1707) ([hicom150](https://github.com/hicom150))
|
|
||||||
- Fix percentage rounding error [\#1716](https://github.com/webtorrent/webtorrent-desktop/pull/1716) ([pR0Ps](https://github.com/pR0Ps))
|
|
||||||
- Fix path-selector in preferences page [\#1702](https://github.com/webtorrent/webtorrent-desktop/pull/1702) ([314eter](https://github.com/314eter))
|
|
||||||
- Fix path-selector in preferences page [\#1704](https://github.com/webtorrent/webtorrent-desktop/pull/1702) ([mathiasvr](https://github.com/mathiasvr))
|
|
||||||
- Fix: Increase height of 'About' window [\#1737](https://github.com/webtorrent/webtorrent-desktop/pull/1737) ([akaustav](https://github.com/akaustav))
|
|
||||||
- Fix "Save Torrent File As..." [\#1743](https://github.com/webtorrent/webtorrent-desktop/pull/1743) ([gpatarin](https://github.com/gpatarin))
|
|
||||||
|
|
||||||
## v0.21.0 - 2019-09-14
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -35,7 +35,7 @@ Download the latest version of WebTorrent Desktop from
|
|||||||
- Use [Homebrew-Cask](https://github.com/caskroom/homebrew-cask) to install from the command line:
|
- Use [Homebrew-Cask](https://github.com/caskroom/homebrew-cask) to install from the command line:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ brew install --cask webtorrent
|
$ brew cask install webtorrent
|
||||||
```
|
```
|
||||||
|
|
||||||
- Try the (unstable) development version by cloning the Git repository. See the
|
- Try the (unstable) development version by cloning the Git repository. See the
|
||||||
@@ -97,7 +97,7 @@ comparing each one to a reference. Why screenshots?
|
|||||||
https://github.com/blog/817-behold-image-view-modes
|
https://github.com/blog/817-behold-image-view-modes
|
||||||
|
|
||||||
For MacOS, you'll need a Retina screen for the integration tests to pass. Your screen should have
|
For MacOS, you'll need a Retina screen for the integration tests to pass. Your screen should have
|
||||||
the same resolution as a 2018 MacBook Pro 13".
|
the same resolution as a 2016 12" Macbook.
|
||||||
|
|
||||||
For Windows, you'll need Windows 10 with a 1366x768 screen.
|
For Windows, you'll need Windows 10 with a 1366x768 screen.
|
||||||
|
|
||||||
@@ -125,7 +125,6 @@ The following optional arguments are available:
|
|||||||
- `--sign` - Sign the application (Mac, Windows)
|
- `--sign` - Sign the application (Mac, Windows)
|
||||||
- `--package=[type]` - Package single output type.
|
- `--package=[type]` - Package single output type.
|
||||||
- `deb` - Debian package
|
- `deb` - Debian package
|
||||||
- `rpm` - RedHat package
|
|
||||||
- `zip` - Linux zip file
|
- `zip` - Linux zip file
|
||||||
- `dmg` - Mac disk image
|
- `dmg` - Mac disk image
|
||||||
- `exe` - Windows installer
|
- `exe` - Windows installer
|
||||||
@@ -142,12 +141,11 @@ The Windows app can be packaged from **any** platform.
|
|||||||
Note: Windows code signing only works from **Windows**, for now.
|
Note: Windows code signing only works from **Windows**, for now.
|
||||||
|
|
||||||
Note: To package the Windows app from non-Windows platforms,
|
Note: To package the Windows app from non-Windows platforms,
|
||||||
[Wine](https://www.winehq.org/) and [Mono](https://www.mono-project.com/) need
|
[Wine](https://www.winehq.org/) needs to be installed. For example on Mac, first
|
||||||
to be installed. For example on Mac, first install
|
install [XQuartz](http://www.xquartz.org/), then run:
|
||||||
[XQuartz](http://www.xquartz.org/), then run:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
brew install wine mono
|
brew install wine
|
||||||
```
|
```
|
||||||
|
|
||||||
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
||||||
@@ -165,6 +163,7 @@ If packaging from Mac, install system dependencies with Homebrew by running:
|
|||||||
```
|
```
|
||||||
npm run install-system-deps
|
npm run install-system-deps
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Recommended readings to start working in the app
|
#### Recommended readings to start working in the app
|
||||||
|
|
||||||
Electron (Framework to make native apps for Windows, OSX and Linux in Javascript):
|
Electron (Framework to make native apps for Windows, OSX and Linux in Javascript):
|
||||||
@@ -174,7 +173,7 @@ React.js (Framework to work with Frontend UI):
|
|||||||
https://reactjs.org/docs/getting-started.html
|
https://reactjs.org/docs/getting-started.html
|
||||||
|
|
||||||
Material UI (React components that implement Google's Material Design.):
|
Material UI (React components that implement Google's Material Design.):
|
||||||
https://material-ui.com/getting-started/installation
|
https://material-ui.com/getting-started
|
||||||
|
|
||||||
### Privacy
|
### Privacy
|
||||||
|
|
||||||
|
|||||||
@@ -46,11 +46,6 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
npm run package -- darwin --sign
|
npm run package -- darwin --sign
|
||||||
```
|
|
||||||
|
|
||||||
Move the `.zip` and `.dmg` file somewhere because the next step wipes the `dist/` folder away.
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run package -- linux --sign
|
npm run package -- linux --sign
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -74,29 +69,17 @@
|
|||||||
|
|
||||||
**This is the most important part.**
|
**This is the most important part.**
|
||||||
|
|
||||||
- Manually download the binaries for each platform from Github.
|
- Manually download the binaries for each platform from Github.
|
||||||
|
|
||||||
**Do not use your locally built binaries.** Modern OSs treat executables differently if they've
|
**Do not use your locally built binaries.** Modern OSs treat executables differently if they've
|
||||||
been downloaded, even though the files are byte for byte identical. This ensures that the
|
been downloaded, even though the files are byte for byte identical. This ensures that the
|
||||||
codesigning worked and is valid.
|
codesigning worked and is valid.
|
||||||
|
|
||||||
- Smoke test WebTorrent Desktop on each platform. Before a release, check that the following basic use cases work correctly:
|
- Smoke test WebTorrent Desktop on each platform.
|
||||||
|
|
||||||
1. Click "Play" to stream a built-in torrent (e.g. Sintel)
|
See Smoke Tests below for details. Open DevTools
|
||||||
- Ensure that seeking to undownloaded region works and plays immediately.
|
on Windows and Mac, and ensure that the auto updater is running. If the auto updater does not
|
||||||
- Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
|
run, users will successfully auto update to this new version, and then be stuck there forever.
|
||||||
|
|
||||||
2. Check that the auto-updater works
|
|
||||||
- Open the console and check for the line "No update available" to indicate that the auto-updater is working. (If the auto updater does not run, users will successfully auto update to this new version, and then be stuck there forever.)
|
|
||||||
|
|
||||||
3. Add a new .torrent file via drag-and-drop.
|
|
||||||
- Ensure that it gets added to the list and starts downloading.
|
|
||||||
|
|
||||||
4. Remove a torrent from the client
|
|
||||||
- Ensure that the file is removed from `~/Downloads`
|
|
||||||
|
|
||||||
5. Create and seed a new a torrent via drag-and-drop.
|
|
||||||
- Ensure that the torrent gets created and seeding begins.
|
|
||||||
|
|
||||||
### 4. Ship it
|
### 4. Ship it
|
||||||
|
|
||||||
@@ -105,5 +88,25 @@
|
|||||||
Create a pull request in [webtorrent.io](https://github.com/webtorrent/webtorrent.io). Update
|
Create a pull request in [webtorrent.io](https://github.com/webtorrent/webtorrent.io). Update
|
||||||
`config.js`, updating the desktop app version.
|
`config.js`, updating the desktop app version.
|
||||||
|
|
||||||
Once this PR is merged and Feross redeploys the WebTorrent website,
|
As soon as this PR is merged, Jenkins will automatically redeploy the WebTorrent website, and
|
||||||
hundreds of thousands of users around the world will start auto updating. **Merge with care.**
|
hundreds of thousands of users around the world will start auto updating. **Merge with care.**
|
||||||
|
|
||||||
|
## Smoke Tests
|
||||||
|
|
||||||
|
Before a release, check that the following basic use cases work correctly:
|
||||||
|
|
||||||
|
1. Click "Play" to stream a built-in torrent (e.g. Sintel)
|
||||||
|
- Ensure that seeking to undownloaded region works and plays immediately.
|
||||||
|
- Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
|
||||||
|
|
||||||
|
2. Check that the auto-updater works
|
||||||
|
- Open the console and check for the line "No update available" to indicate
|
||||||
|
|
||||||
|
3. Add a new .torrent file via drag-and-drop.
|
||||||
|
- Ensure that it gets added to the list and starts downloading
|
||||||
|
|
||||||
|
4. Remove a torrent from the client
|
||||||
|
- Ensure that the file is removed from `~/Downloads`
|
||||||
|
|
||||||
|
5. Create and seed a new a torrent via drag-and-drop.
|
||||||
|
- Ensure that the torrent gets created and seeding begins.
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.debugger</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
39
bin/extra-lint.js
Executable file
39
bin/extra-lint.js
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const walkSync = require('walk-sync')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
let hasErrors = false
|
||||||
|
|
||||||
|
// Find all Javascript source files
|
||||||
|
const files = walkSync('src', { globs: ['**/*.js'] })
|
||||||
|
console.log('Running extra-lint on ' + files.length + ' files...')
|
||||||
|
|
||||||
|
// Read each file, line by line
|
||||||
|
files.forEach(function (file) {
|
||||||
|
const filepath = path.join('src', file)
|
||||||
|
const lines = fs.readFileSync(filepath, 'utf8').split('\n')
|
||||||
|
|
||||||
|
lines.forEach(function (line, i) {
|
||||||
|
let error
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
const 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!')
|
||||||
115
bin/package.js
115
bin/package.js
@@ -8,6 +8,7 @@ const cp = require('child_process')
|
|||||||
const electronPackager = require('electron-packager')
|
const electronPackager = require('electron-packager')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const minimist = require('minimist')
|
const minimist = require('minimist')
|
||||||
|
const mkdirp = require('mkdirp')
|
||||||
const os = require('os')
|
const os = require('os')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const rimraf = require('rimraf')
|
const rimraf = require('rimraf')
|
||||||
@@ -36,7 +37,7 @@ const argv = minimist(process.argv.slice(2), {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function build () {
|
function build () {
|
||||||
console.log('Installing node_modules...')
|
console.log('Reinstalling node_modules...')
|
||||||
rimraf.sync(NODE_MODULES_PATH)
|
rimraf.sync(NODE_MODULES_PATH)
|
||||||
cp.execSync('npm ci', { stdio: 'inherit' })
|
cp.execSync('npm ci', { stdio: 'inherit' })
|
||||||
|
|
||||||
@@ -172,8 +173,8 @@ const linux = {
|
|||||||
// Build for Linux.
|
// Build for Linux.
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
|
|
||||||
// Build x64, armv7l, and arm64 binaries.
|
// Build x64 binary onle.
|
||||||
arch: ['x64', 'armv7l', 'arm64']
|
arch: 'x64'
|
||||||
|
|
||||||
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
||||||
}
|
}
|
||||||
@@ -266,7 +267,6 @@ function buildDarwin (cb) {
|
|||||||
|
|
||||||
function signApp (cb) {
|
function signApp (cb) {
|
||||||
const sign = require('electron-osx-sign')
|
const sign = require('electron-osx-sign')
|
||||||
const { notarize } = require('electron-notarize')
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
|
* Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
|
||||||
@@ -282,37 +282,16 @@ function buildDarwin (cb) {
|
|||||||
* - Membership in the Apple Developer Program
|
* - Membership in the Apple Developer Program
|
||||||
*/
|
*/
|
||||||
const signOpts = {
|
const signOpts = {
|
||||||
verbose: true,
|
|
||||||
app: appPath,
|
app: appPath,
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
identity: 'Developer ID Application: WebTorrent, LLC (5MAMC8G3L8)',
|
verbose: true
|
||||||
hardenedRuntime: true,
|
|
||||||
entitlements: path.join(config.ROOT_PATH, 'bin', 'darwin-entitlements.plist'),
|
|
||||||
'entitlements-inherit': path.join(config.ROOT_PATH, 'bin', 'darwin-entitlements.plist'),
|
|
||||||
'signature-flags': 'library'
|
|
||||||
}
|
|
||||||
|
|
||||||
const notarizeOpts = {
|
|
||||||
appBundleId: darwin.appBundleId,
|
|
||||||
appPath,
|
|
||||||
appleId: 'feross@feross.org',
|
|
||||||
appleIdPassword: '@keychain:AC_PASSWORD'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Mac: Signing app...')
|
console.log('Mac: Signing app...')
|
||||||
sign(signOpts, function (err) {
|
sign(signOpts, function (err) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log('Mac: Signed app.')
|
console.log('Mac: Signed app.')
|
||||||
|
cb(null)
|
||||||
console.log('Mac: Notarizing app...')
|
|
||||||
notarize(notarizeOpts).then(
|
|
||||||
function () {
|
|
||||||
console.log('Mac: Notarized app.')
|
|
||||||
cb(null)
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
cb(err)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +430,7 @@ function buildWin32 (cb) {
|
|||||||
// remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
|
// remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
|
||||||
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
||||||
setupIcon: config.APP_ICON + '.ico',
|
setupIcon: config.APP_ICON + '.ico',
|
||||||
signWithParams,
|
signWithParams: signWithParams,
|
||||||
title: config.APP_NAME,
|
title: config.APP_NAME,
|
||||||
usePackageJson: false,
|
usePackageJson: false,
|
||||||
version: pkg.version
|
version: pkg.version
|
||||||
@@ -478,13 +457,13 @@ function buildWin32 (cb) {
|
|||||||
console.log('Windows: Creating portable app...')
|
console.log('Windows: Creating portable app...')
|
||||||
|
|
||||||
const portablePath = path.join(filesPath, 'Portable Settings')
|
const portablePath = path.join(filesPath, 'Portable Settings')
|
||||||
fs.mkdirSync(portablePath, { recursive: true })
|
mkdirp.sync(portablePath)
|
||||||
|
|
||||||
const downloadsPath = path.join(portablePath, 'Downloads')
|
const downloadsPath = path.join(portablePath, 'Downloads')
|
||||||
fs.mkdirSync(downloadsPath, { recursive: true })
|
mkdirp.sync(downloadsPath)
|
||||||
|
|
||||||
const tempPath = path.join(portablePath, 'Temp')
|
const tempPath = path.join(portablePath, 'Temp')
|
||||||
fs.mkdirSync(tempPath, { recursive: true })
|
mkdirp.sync(tempPath)
|
||||||
|
|
||||||
const inPath = path.join(DIST_PATH, path.basename(filesPath))
|
const inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||||
const outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
const outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
||||||
@@ -506,16 +485,11 @@ function buildLinux (cb) {
|
|||||||
|
|
||||||
const tasks = []
|
const tasks = []
|
||||||
buildPath.forEach(function (filesPath) {
|
buildPath.forEach(function (filesPath) {
|
||||||
const destArch = filesPath.split('-').pop()
|
|
||||||
|
|
||||||
if (argv.package === 'deb' || argv.package === 'all') {
|
if (argv.package === 'deb' || argv.package === 'all') {
|
||||||
tasks.push((cb) => packageDeb(filesPath, destArch, cb))
|
tasks.push((cb) => packageDeb(filesPath, cb))
|
||||||
}
|
|
||||||
if (argv.package === 'rpm' || argv.package === 'all') {
|
|
||||||
tasks.push((cb) => packageRpm(filesPath, destArch, cb))
|
|
||||||
}
|
}
|
||||||
if (argv.package === 'zip' || argv.package === 'all') {
|
if (argv.package === 'zip' || argv.package === 'all') {
|
||||||
tasks.push((cb) => packageZip(filesPath, destArch, cb))
|
tasks.push((cb) => packageZip(filesPath, cb))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
series(tasks, cb)
|
series(tasks, cb)
|
||||||
@@ -523,65 +497,16 @@ function buildLinux (cb) {
|
|||||||
cb(err)
|
cb(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
function packageDeb (filesPath, destArch, cb) {
|
function packageDeb (filesPath, cb) {
|
||||||
// Linux convention for Debian based 'x64' is 'amd64'
|
|
||||||
if (destArch === 'x64') {
|
|
||||||
destArch = 'amd64'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create .deb file for Debian-based platforms
|
// Create .deb file for Debian-based platforms
|
||||||
console.log(`Linux: Creating ${destArch} deb...`)
|
console.log('Linux: Creating deb...')
|
||||||
|
|
||||||
const installer = require('electron-installer-debian')
|
const installer = require('electron-installer-debian')
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
src: filesPath + '/',
|
src: filesPath + '/',
|
||||||
dest: DIST_PATH,
|
dest: DIST_PATH,
|
||||||
arch: destArch,
|
arch: 'amd64',
|
||||||
bin: 'WebTorrent',
|
|
||||||
icon: {
|
|
||||||
'48x48': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/48x48/apps/webtorrent-desktop.png'),
|
|
||||||
'256x256': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/256x256/apps/webtorrent-desktop.png')
|
|
||||||
},
|
|
||||||
categories: ['Network', 'FileTransfer', 'P2P'],
|
|
||||||
mimeType: ['application/x-bittorrent', 'x-scheme-handler/magnet', 'x-scheme-handler/stream-magnet'],
|
|
||||||
desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs'),
|
|
||||||
lintianOverrides: [
|
|
||||||
'unstripped-binary-or-object',
|
|
||||||
'embedded-library',
|
|
||||||
'missing-dependency-on-libc',
|
|
||||||
'changelog-file-missing-in-native-package',
|
|
||||||
'description-synopsis-is-duplicated',
|
|
||||||
'setuid-binary',
|
|
||||||
'binary-without-manpage',
|
|
||||||
'shlib-with-executable-bit'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
installer(options).then(
|
|
||||||
() => {
|
|
||||||
console.log(`Linux: Created ${destArch} deb.`)
|
|
||||||
cb(null)
|
|
||||||
},
|
|
||||||
(err) => cb(err)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function packageRpm (filesPath, destArch, cb) {
|
|
||||||
// Linux convention for RedHat based 'x64' is 'x86_64'
|
|
||||||
if (destArch === 'x64') {
|
|
||||||
destArch = 'x86_64'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create .rpm file for RedHat-based platforms
|
|
||||||
console.log(`Linux: Creating ${destArch} rpm...`)
|
|
||||||
|
|
||||||
const installer = require('electron-installer-redhat')
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
src: filesPath + '/',
|
|
||||||
dest: DIST_PATH,
|
|
||||||
arch: destArch,
|
|
||||||
bin: 'WebTorrent',
|
bin: 'WebTorrent',
|
||||||
icon: {
|
icon: {
|
||||||
'48x48': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/48x48/apps/webtorrent-desktop.png'),
|
'48x48': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/48x48/apps/webtorrent-desktop.png'),
|
||||||
@@ -594,22 +519,22 @@ function buildLinux (cb) {
|
|||||||
|
|
||||||
installer(options).then(
|
installer(options).then(
|
||||||
() => {
|
() => {
|
||||||
console.log(`Linux: Created ${destArch} rpm.`)
|
console.log('Linux: Created deb.')
|
||||||
cb(null)
|
cb(null)
|
||||||
},
|
},
|
||||||
(err) => cb(err)
|
(err) => cb(err)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function packageZip (filesPath, destArch, cb) {
|
function packageZip (filesPath, cb) {
|
||||||
// Create .zip file for Linux
|
// Create .zip file for Linux
|
||||||
console.log(`Linux: Creating ${destArch} zip...`)
|
console.log('Linux: Creating zip...')
|
||||||
|
|
||||||
const inPath = path.join(DIST_PATH, path.basename(filesPath))
|
const inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||||
const outPath = path.join(DIST_PATH, `${BUILD_NAME}-linux-${destArch}.zip`)
|
const outPath = path.join(DIST_PATH, BUILD_NAME + '-linux.zip')
|
||||||
zip.zipSync(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log(`Linux: Created ${destArch} zip.`)
|
console.log('Linux: Created zip.')
|
||||||
cb(null)
|
cb(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ while (<>) {
|
|||||||
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)/;
|
next if /(grunjol\@users.noreply.github.com)/;
|
||||||
next if /(dependabot)/;
|
|
||||||
$seen{$_} = push @authors, "- ", $_;
|
$seen{$_} = push @authors, "- ", $_;
|
||||||
}
|
}
|
||||||
END {
|
END {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
module.exports = require('electron')
|
|
||||||
26184
package-lock.json
generated
26184
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
146
package.json
146
package.json
@@ -1,89 +1,77 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"type": "module",
|
|
||||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||||
"version": "0.24.0",
|
"version": "0.20.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@webtorrent.io",
|
"email": "feross@webtorrent.io",
|
||||||
"url": "https://webtorrent.io"
|
"url": "https://webtorrent.io"
|
||||||
},
|
},
|
||||||
"babel": {
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"@babel/plugin-transform-react-jsx",
|
|
||||||
{
|
|
||||||
"useBuiltIns": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/webtorrent/webtorrent-desktop/issues"
|
"url": "https://github.com/webtorrent/webtorrent-desktop/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/remote": "2.0.1",
|
|
||||||
"airplayer": "github:webtorrent/airplayer#fix-security",
|
"airplayer": "github:webtorrent/airplayer#fix-security",
|
||||||
"application-config": "2.0.0",
|
"application-config": "^1.0.1",
|
||||||
"arch": "2.2.0",
|
"arch": "^2.1.1",
|
||||||
"auto-launch": "5.0.5",
|
"auto-launch": "^5.0.5",
|
||||||
"bitfield": "4.0.0",
|
"bitfield": "^3.0.0",
|
||||||
"capture-frame": "4.0.0",
|
"capture-frame": "^3.0.0",
|
||||||
"chokidar": "3.5.2",
|
"chokidar": "^3.0.2",
|
||||||
"chromecasts": "1.10.2",
|
"chromecasts": "^1.9.1",
|
||||||
"create-torrent": "5.0.1",
|
"cp-file": "^7.0.0",
|
||||||
"debounce": "1.2.1",
|
"create-torrent": "^4.4.1",
|
||||||
"dlnacasts": "0.1.0",
|
"debounce": "^1.2.0",
|
||||||
"drag-drop": "7.2.0",
|
"deep-equal": "^1.1.0",
|
||||||
"es6-error": "4.1.1",
|
"dlnacasts": "^0.1.0",
|
||||||
"fn-getter": "1.0.0",
|
"drag-drop": "^5.0.1",
|
||||||
"iso-639-1": "2.1.9",
|
"es6-error": "^4.1.1",
|
||||||
"languagedetect": "2.0.0",
|
"fn-getter": "^1.0.0",
|
||||||
"location-history": "1.1.2",
|
"iso-639-1": "^2.1.0",
|
||||||
"material-ui": "0.20.2",
|
"languagedetect": "^1.2.0",
|
||||||
"music-metadata": "7.11.4",
|
"location-history": "^1.1.1",
|
||||||
"network-address": "1.1.2",
|
"material-ui": "^0.20.2",
|
||||||
"parse-torrent": "9.1.4",
|
"mkdirp": "^0.5.1",
|
||||||
"prettier-bytes": "1.0.4",
|
"music-metadata": "^4.5.3",
|
||||||
"prop-types": "15.7.2",
|
"network-address": "^1.1.2",
|
||||||
"react": "17.0.2",
|
"parse-torrent": "^7.0.1",
|
||||||
"react-dom": "17.0.2",
|
"prettier-bytes": "^1.0.4",
|
||||||
"rimraf": "3.0.2",
|
"prop-types": "^15.7.2",
|
||||||
"run-parallel": "1.2.0",
|
"react": "^16.9.0",
|
||||||
"semver": "7.3.5",
|
"react-dom": "^16.9.0",
|
||||||
"simple-concat": "1.0.1",
|
"rimraf": "^3.0.0",
|
||||||
"simple-get": "4.0.0",
|
"run-parallel": "^1.1.9",
|
||||||
"srt-to-vtt": "1.1.3",
|
"semver": "^6.3.0",
|
||||||
"vlc-command": "1.2.0",
|
"simple-concat": "^1.0.0",
|
||||||
"webtorrent": "1.5.6",
|
"simple-get": "^3.0.3",
|
||||||
"winreg": "1.2.4"
|
"srt-to-vtt": "^1.1.3",
|
||||||
|
"vlc-command": "^1.2.0",
|
||||||
|
"webtorrent": ">=0.107.16",
|
||||||
|
"winreg": "^1.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "7.15.7",
|
"babel-eslint": "^10.0.3",
|
||||||
"@babel/core": "7.15.8",
|
"buble": "^0.19.8",
|
||||||
"@babel/eslint-parser": "7.15.8",
|
"cross-zip": "^2.1.6",
|
||||||
"@babel/plugin-transform-react-jsx": "7.14.9",
|
"depcheck": "^0.8.3",
|
||||||
"cross-zip": "4.0.0",
|
"electron": "~6.0.9",
|
||||||
"depcheck": "1.4.2",
|
"electron-osx-sign": "^0.4.13",
|
||||||
"electron": "15.2.0",
|
"electron-packager": "^14.0.6",
|
||||||
"electron-notarize": "1.1.1",
|
"electron-winstaller": "^4.0.0",
|
||||||
"electron-osx-sign": "0.5.0",
|
"gh-release": "^3.5.0",
|
||||||
"electron-packager": "15.4.0",
|
"minimist": "^1.2.0",
|
||||||
"electron-winstaller": "5.0.0",
|
"nodemon": "^1.19.2",
|
||||||
"gh-release": "6.0.1",
|
"open": "^6.4.0",
|
||||||
"minimist": "1.2.5",
|
"plist": "^3.0.1",
|
||||||
"nodemon": "2.0.13",
|
"pngjs": "^3.4.0",
|
||||||
"open": "8.3.0",
|
"run-series": "^1.1.8",
|
||||||
"plist": "3.0.4",
|
"spectron": "^8.0.0",
|
||||||
"pngjs": "6.0.0",
|
"standard": "*",
|
||||||
"run-series": "1.1.9",
|
"tape": "^4.11.0",
|
||||||
"spectron": "15.0.0",
|
"walk-sync": "^2.0.2"
|
||||||
"standard": "16.0.4",
|
|
||||||
"tape": "5.3.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.0.0",
|
"node": ">=4.0.0"
|
||||||
"npm": "^7.10.0 || ^8.0.0"
|
|
||||||
},
|
},
|
||||||
"homepage": "https://webtorrent.io",
|
"homepage": "https://webtorrent.io",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -97,11 +85,10 @@
|
|||||||
"webtorrent"
|
"webtorrent"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.cjs",
|
"main": "index.js",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"appdmg": "^0.6.0",
|
"appdmg": "^0.6.0",
|
||||||
"electron-installer-debian": "^3.1.0",
|
"electron-installer-debian": "^2.0.0"
|
||||||
"electron-installer-redhat": "^3.3.0"
|
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "WebTorrent",
|
"productName": "WebTorrent",
|
||||||
@@ -110,24 +97,19 @@
|
|||||||
"url": "git://github.com/webtorrent/webtorrent-desktop.git"
|
"url": "git://github.com/webtorrent/webtorrent-desktop.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "babel src --out-dir build",
|
"build": "buble src --output build",
|
||||||
"clean": "node ./bin/clean.js",
|
"clean": "node ./bin/clean.js",
|
||||||
"gh-release": "gh-release",
|
"gh-release": "gh-release",
|
||||||
"install-system-deps": "brew install fakeroot dpkg rpm",
|
"install-system-deps": "brew install fakeroot dpkg",
|
||||||
"open-config": "node ./bin/open-config.js",
|
"open-config": "node ./bin/open-config.js",
|
||||||
"package": "node ./bin/package.js",
|
"package": "node ./bin/package.js",
|
||||||
"start": "npm run build && electron --no-sandbox .",
|
"start": "npm run build && electron .",
|
||||||
"test": "standard && depcheck --ignores=standard,@babel/eslint-parser --ignore-dirs=build,dist",
|
"test": "standard && depcheck --ignores=standard,babel-eslint --ignore-dirs=build,dist && node ./bin/extra-lint.js",
|
||||||
"test-integration": "npm run build && node ./test",
|
"test-integration": "npm run build && node ./test",
|
||||||
"update-authors": "./bin/update-authors.sh",
|
"update-authors": "./bin/update-authors.sh",
|
||||||
"watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/"
|
"watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/"
|
||||||
},
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"parser": "@babel/eslint-parser"
|
"parser": "babel-eslint"
|
||||||
},
|
|
||||||
"renovate": {
|
|
||||||
"extends": [
|
|
||||||
"github>webtorrent/renovate-config"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
230
src/config.js
230
src/config.js
@@ -1,13 +1,16 @@
|
|||||||
import applicationConfig from 'application-config'
|
const appConfig = require('application-config')('WebTorrent')
|
||||||
import path from 'path'
|
appConfig.filePath = appConfig.filePath.replace('\\Local\\', '\\Roaming\\')
|
||||||
import arch from 'arch'
|
const path = require('path')
|
||||||
import fs from 'fs'
|
const electron = require('electron')
|
||||||
import electron from '../electron.cjs'
|
const arch = require('arch')
|
||||||
|
|
||||||
|
// console.log('electron appData: ', (electron.remote ? electron.remote : electron).app.getPath('appData'))
|
||||||
|
console.log('appConfig.filePath', appConfig.filePath)
|
||||||
|
|
||||||
const appConfig = applicationConfig('WebTorrent')
|
|
||||||
const APP_NAME = 'WebTorrent'
|
const APP_NAME = 'WebTorrent'
|
||||||
const APP_TEAM = 'WebTorrent, LLC'
|
const APP_TEAM = 'WebTorrent, LLC'
|
||||||
const APP_VERSION = JSON.parse(fs.readFileSync('package.json').toString()).version
|
const APP_VERSION = require('../package.json').version
|
||||||
|
|
||||||
const IS_TEST = isTest()
|
const IS_TEST = isTest()
|
||||||
const PORTABLE_PATH = IS_TEST
|
const PORTABLE_PATH = IS_TEST
|
||||||
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
|
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
|
||||||
@@ -18,6 +21,93 @@ const IS_PORTABLE = isPortable()
|
|||||||
const UI_HEADER_HEIGHT = 38
|
const UI_HEADER_HEIGHT = 38
|
||||||
const UI_TORRENT_HEIGHT = 100
|
const UI_TORRENT_HEIGHT = 100
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||||
|
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
|
||||||
|
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||||
|
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
|
||||||
|
|
||||||
|
APP_COPYRIGHT: 'Copyright © 2014-2019 ' + APP_TEAM,
|
||||||
|
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
|
||||||
|
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
|
||||||
|
APP_NAME: APP_NAME,
|
||||||
|
APP_TEAM: APP_TEAM,
|
||||||
|
APP_VERSION: APP_VERSION,
|
||||||
|
APP_WINDOW_TITLE: APP_NAME,
|
||||||
|
|
||||||
|
CONFIG_PATH: getConfigPath(),
|
||||||
|
|
||||||
|
DEFAULT_TORRENTS: [
|
||||||
|
// {
|
||||||
|
// testID: 'bbb',
|
||||||
|
// name: 'Big Buck Bunny',
|
||||||
|
// posterFileName: 'bigBuckBunny.jpg',
|
||||||
|
// torrentFileName: 'bigBuckBunny.torrent'
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
testID: 'cosmos',
|
||||||
|
name: 'Cosmos Laundromat (Preview)',
|
||||||
|
posterFileName: 'cosmosLaundromat.jpg',
|
||||||
|
torrentFileName: 'cosmosLaundromat.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testID: 'sintel',
|
||||||
|
name: 'Sintel',
|
||||||
|
posterFileName: 'sintel.jpg',
|
||||||
|
torrentFileName: 'sintel.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testID: 'tears',
|
||||||
|
name: 'Tears of Steel',
|
||||||
|
posterFileName: 'tearsOfSteel.jpg',
|
||||||
|
torrentFileName: 'tearsOfSteel.torrent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testID: 'wired',
|
||||||
|
name: 'The WIRED CD - Rip. Sample. Mash. Share',
|
||||||
|
posterFileName: 'wiredCd.jpg',
|
||||||
|
torrentFileName: 'wiredCd.torrent'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
DELAYED_INIT: 3000 /* 3 seconds */,
|
||||||
|
|
||||||
|
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
|
||||||
|
|
||||||
|
GITHUB_URL: 'https://github.com/webtorrent/webtorrent-desktop',
|
||||||
|
GITHUB_URL_ISSUES: 'https://github.com/webtorrent/webtorrent-desktop/issues',
|
||||||
|
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/webtorrent/webtorrent-desktop/master',
|
||||||
|
GITHUB_URL_RELEASES: 'https://github.com/webtorrent/webtorrent-desktop/releases',
|
||||||
|
|
||||||
|
HOME_PAGE_URL: 'https://webtorrent.io',
|
||||||
|
TWITTER_PAGE_URL: 'https://twitter.com/WebTorrentApp',
|
||||||
|
|
||||||
|
IS_PORTABLE: IS_PORTABLE,
|
||||||
|
IS_PRODUCTION: IS_PRODUCTION,
|
||||||
|
IS_TEST: IS_TEST,
|
||||||
|
|
||||||
|
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
|
||||||
|
|
||||||
|
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
|
ROOT_PATH: path.join(__dirname, '..'),
|
||||||
|
STATIC_PATH: path.join(__dirname, '..', 'static'),
|
||||||
|
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||||
|
|
||||||
|
WINDOW_ABOUT: 'file://' + path.join(__dirname, '..', 'static', 'about.html'),
|
||||||
|
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
|
||||||
|
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
|
||||||
|
|
||||||
|
WINDOW_INITIAL_BOUNDS: {
|
||||||
|
width: 500,
|
||||||
|
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
|
||||||
|
},
|
||||||
|
WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents
|
||||||
|
WINDOW_MIN_WIDTH: 425,
|
||||||
|
|
||||||
|
UI_HEADER_HEIGHT: UI_HEADER_HEIGHT,
|
||||||
|
UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT
|
||||||
|
}
|
||||||
|
|
||||||
function getConfigPath () {
|
function getConfigPath () {
|
||||||
if (IS_PORTABLE) {
|
if (IS_PORTABLE) {
|
||||||
return PORTABLE_PATH
|
return PORTABLE_PATH
|
||||||
@@ -35,12 +125,16 @@ function getDefaultDownloadPath () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPath (key) {
|
function getPath (key) {
|
||||||
if (!process.versions.electron || process.type !== 'browser') {
|
if (!process.versions.electron) {
|
||||||
// Node.js process
|
// Node.js process
|
||||||
return ''
|
return ''
|
||||||
|
} else if (process.type === 'renderer') {
|
||||||
|
// Electron renderer process
|
||||||
|
return electron.remote.app.getPath(key)
|
||||||
|
} else {
|
||||||
|
// Electron main process
|
||||||
|
return electron.app.getPath(key)
|
||||||
}
|
}
|
||||||
// Electron main process
|
|
||||||
return electron.app.getPath(key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTest () {
|
function isTest () {
|
||||||
@@ -57,6 +151,8 @@ function isPortable () {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This line throws if the "Portable Settings" folder does not exist, and does
|
// This line throws if the "Portable Settings" folder does not exist, and does
|
||||||
// nothing otherwise.
|
// nothing otherwise.
|
||||||
@@ -82,117 +178,3 @@ function isProduction () {
|
|||||||
return !/\/electron$/.test(process.execPath)
|
return !/\/electron$/.test(process.execPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ANNOUNCEMENT_URL = 'https://webtorrent.io/desktop/announcement'
|
|
||||||
export const AUTO_UPDATE_URL = 'https://webtorrent.io/desktop/update'
|
|
||||||
export const CRASH_REPORT_URL = 'https://webtorrent.io/desktop/crash-report'
|
|
||||||
export const TELEMETRY_URL = 'https://webtorrent.io/desktop/telemetry'
|
|
||||||
export const APP_COPYRIGHT = `Copyright © 2014-${new Date().getFullYear()} ${APP_TEAM}`
|
|
||||||
export const APP_FILE_ICON = new URL('../static/WebTorrentFile', import.meta.url).pathname // path.join(__dirname, '..',
|
|
||||||
// 'static', 'WebTorrentFile')
|
|
||||||
export const APP_ICON = new URL('../static/WebTorrent', import.meta.url).pathname // path.join(__dirname, '..',
|
|
||||||
// 'static',
|
|
||||||
// 'WebTorrent')
|
|
||||||
export const CONFIG_PATH = getConfigPath()
|
|
||||||
export const DEFAULT_TORRENTS = [
|
|
||||||
{
|
|
||||||
testID: 'bbb',
|
|
||||||
name: 'Big Buck Bunny',
|
|
||||||
posterFileName: 'bigBuckBunny.jpg',
|
|
||||||
torrentFileName: 'bigBuckBunny.torrent'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testID: 'cosmos',
|
|
||||||
name: 'Cosmos Laundromat (Preview)',
|
|
||||||
posterFileName: 'cosmosLaundromat.jpg',
|
|
||||||
torrentFileName: 'cosmosLaundromat.torrent'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testID: 'sintel',
|
|
||||||
name: 'Sintel',
|
|
||||||
posterFileName: 'sintel.jpg',
|
|
||||||
torrentFileName: 'sintel.torrent'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testID: 'tears',
|
|
||||||
name: 'Tears of Steel',
|
|
||||||
posterFileName: 'tearsOfSteel.jpg',
|
|
||||||
torrentFileName: 'tearsOfSteel.torrent'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testID: 'wired',
|
|
||||||
name: 'The WIRED CD - Rip. Sample. Mash. Share',
|
|
||||||
posterFileName: 'wiredCd.jpg',
|
|
||||||
torrentFileName: 'wiredCd.torrent'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
export const DELAYED_INIT = 3000 /* 3 seconds */
|
|
||||||
export const DEFAULT_DOWNLOAD_PATH = getDefaultDownloadPath()
|
|
||||||
export const GITHUB_URL = 'https://github.com/webtorrent/webtorrent-desktop'
|
|
||||||
export const GITHUB_URL_ISSUES = 'https://github.com/webtorrent/webtorrent-desktop/issues'
|
|
||||||
export const GITHUB_URL_RAW = 'https://raw.githubusercontent.com/webtorrent/webtorrent-desktop/master'
|
|
||||||
export const GITHUB_URL_RELEASES = 'https://github.com/webtorrent/webtorrent-desktop/releases'
|
|
||||||
export const HOME_PAGE_URL = 'https://webtorrent.io'
|
|
||||||
export const TWITTER_PAGE_URL = 'https://twitter.com/WebTorrentApp'
|
|
||||||
export const OS_SYSARCH = arch() === 'x64' ? 'x64' : 'ia32'
|
|
||||||
export const POSTER_PATH = path.join(getConfigPath(), 'Posters')
|
|
||||||
export const ROOT_PATH = new URL('../', import.meta.url).pathname
|
|
||||||
export const STATIC_PATH = new URL('../static', import.meta.url).pathname
|
|
||||||
export const TORRENT_PATH = path.join(getConfigPath(), 'Torrents')
|
|
||||||
export const WINDOW_ABOUT = 'file://' + new URL('../static/about.html', import.meta.url).pathname
|
|
||||||
export const WINDOW_MAIN = 'file://' + new URL('../static/main.html', import.meta.url).pathname
|
|
||||||
export const WINDOW_WEBTORRENT = 'file://' + new URL('../static/webtorrent.html', import.meta.url).pathname
|
|
||||||
export const WINDOW_INITIAL_BOUNDS = {
|
|
||||||
width: 500,
|
|
||||||
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
|
|
||||||
}
|
|
||||||
export const WINDOW_MIN_HEIGHT = UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2)
|
|
||||||
export const WINDOW_MIN_WIDTH = 425
|
|
||||||
export { APP_NAME }
|
|
||||||
export { APP_TEAM }
|
|
||||||
export { APP_VERSION }
|
|
||||||
export { APP_NAME as APP_WINDOW_TITLE }
|
|
||||||
export { IS_PORTABLE }
|
|
||||||
export { IS_PRODUCTION }
|
|
||||||
export { IS_TEST }
|
|
||||||
export { UI_HEADER_HEIGHT }
|
|
||||||
export { UI_TORRENT_HEIGHT }
|
|
||||||
export default {
|
|
||||||
ANNOUNCEMENT_URL,
|
|
||||||
AUTO_UPDATE_URL,
|
|
||||||
CRASH_REPORT_URL,
|
|
||||||
TELEMETRY_URL,
|
|
||||||
APP_COPYRIGHT,
|
|
||||||
APP_FILE_ICON,
|
|
||||||
APP_ICON,
|
|
||||||
APP_NAME,
|
|
||||||
APP_TEAM,
|
|
||||||
APP_VERSION,
|
|
||||||
APP_WINDOW_TITLE: APP_NAME,
|
|
||||||
CONFIG_PATH,
|
|
||||||
DEFAULT_TORRENTS,
|
|
||||||
DELAYED_INIT,
|
|
||||||
DEFAULT_DOWNLOAD_PATH,
|
|
||||||
GITHUB_URL,
|
|
||||||
GITHUB_URL_ISSUES,
|
|
||||||
GITHUB_URL_RAW,
|
|
||||||
GITHUB_URL_RELEASES,
|
|
||||||
HOME_PAGE_URL,
|
|
||||||
TWITTER_PAGE_URL,
|
|
||||||
IS_PORTABLE,
|
|
||||||
IS_PRODUCTION,
|
|
||||||
IS_TEST,
|
|
||||||
OS_SYSARCH,
|
|
||||||
POSTER_PATH,
|
|
||||||
ROOT_PATH,
|
|
||||||
STATIC_PATH,
|
|
||||||
TORRENT_PATH,
|
|
||||||
WINDOW_ABOUT,
|
|
||||||
WINDOW_MAIN,
|
|
||||||
WINDOW_WEBTORRENT,
|
|
||||||
WINDOW_INITIAL_BOUNDS,
|
|
||||||
WINDOW_MIN_HEIGHT,
|
|
||||||
WINDOW_MIN_WIDTH,
|
|
||||||
UI_HEADER_HEIGHT,
|
|
||||||
UI_TORRENT_HEIGHT
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import electron from '../electron.cjs'
|
module.exports = {
|
||||||
|
init
|
||||||
async function init () {
|
|
||||||
const config = await import('./config.js')
|
|
||||||
electron.crashReporter.start({
|
|
||||||
productName: config.APP_NAME,
|
|
||||||
submitURL: config.CRASH_REPORT_URL,
|
|
||||||
globalExtra: { _companyName: config.APP_NAME },
|
|
||||||
compress: true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { init }
|
function init () {
|
||||||
|
const config = require('./config')
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
electron.crashReporter.start({
|
||||||
|
companyName: config.APP_NAME,
|
||||||
|
productName: config.APP_NAME,
|
||||||
|
submitURL: config.CRASH_REPORT_URL
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import electron from '../../electron.cjs'
|
module.exports = {
|
||||||
import config from '../config.js'
|
init
|
||||||
import log from './log.js'
|
}
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const config = require('../config')
|
||||||
|
const log = require('./log')
|
||||||
|
|
||||||
const ANNOUNCEMENT_URL =
|
const ANNOUNCEMENT_URL =
|
||||||
`${config.ANNOUNCEMENT_URL}?version=${config.APP_VERSION}&platform=${process.platform}`
|
`${config.ANNOUNCEMENT_URL}?version=${config.APP_VERSION}&platform=${process.platform}`
|
||||||
@@ -19,8 +24,8 @@ const ANNOUNCEMENT_URL =
|
|||||||
* "detail": "Please update to v0.xx as soon as possible..."
|
* "detail": "Please update to v0.xx as soon as possible..."
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
async function init () {
|
function init () {
|
||||||
const { default: get } = await import('simple-get')
|
const get = require('simple-get')
|
||||||
get.concat(ANNOUNCEMENT_URL, onResponse)
|
get.concat(ANNOUNCEMENT_URL, onResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +50,7 @@ function onResponse (err, res, data) {
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
message: data.message,
|
message: data.message,
|
||||||
detail: data.detail
|
detail: data.detail
|
||||||
})
|
}, noop)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { init }
|
function noop () {}
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import electron from '../../electron.cjs'
|
module.exports = {
|
||||||
import log from './log.js'
|
openSeedFile,
|
||||||
import * as windows from './windows/index.js'
|
openSeedDirectory,
|
||||||
|
openTorrentFile,
|
||||||
|
openTorrentAddress,
|
||||||
|
openFiles
|
||||||
|
}
|
||||||
|
|
||||||
const { dialog } = electron
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const log = require('./log')
|
||||||
|
const windows = require('./windows')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show open dialog to create a single-file torrent.
|
* Show open dialog to create a single-file torrent.
|
||||||
*/
|
*/
|
||||||
export function openSeedFile () {
|
function openSeedFile () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openSeedFile')
|
log('openSeedFile')
|
||||||
const opts = {
|
const opts = {
|
||||||
@@ -22,18 +29,18 @@ export function openSeedFile () {
|
|||||||
* Windows and Linux, open dialogs are for files *or* directories only, not both,
|
* Windows and Linux, open dialogs are for files *or* directories only, not both,
|
||||||
* so this function shows a directory dialog on those platforms.
|
* so this function shows a directory dialog on those platforms.
|
||||||
*/
|
*/
|
||||||
export function openSeedDirectory () {
|
function openSeedDirectory () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openSeedDirectory')
|
log('openSeedDirectory')
|
||||||
const 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']
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: 'Select a folder for the torrent.',
|
title: 'Select a folder for the torrent.',
|
||||||
properties: ['openDirectory']
|
properties: ['openDirectory']
|
||||||
}
|
}
|
||||||
showOpenSeed(opts)
|
showOpenSeed(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,29 +48,30 @@ export function openSeedDirectory () {
|
|||||||
* Show flexible open dialog that supports selecting .torrent files to add, or
|
* Show flexible open dialog that supports selecting .torrent files to add, or
|
||||||
* a file or folder to create a single-file or single-directory torrent.
|
* a file or folder to create a single-file or single-directory torrent.
|
||||||
*/
|
*/
|
||||||
export function openFiles () {
|
function openFiles () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openFiles')
|
log('openFiles')
|
||||||
const 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']
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: 'Select a file to add.',
|
title: 'Select a file to add.',
|
||||||
properties: ['openFile']
|
properties: ['openFile']
|
||||||
}
|
}
|
||||||
setTitle(opts.title)
|
setTitle(opts.title)
|
||||||
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
|
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||||
resetTitle()
|
resetTitle()
|
||||||
if (!Array.isArray(selectedPaths)) return
|
if (!Array.isArray(selectedPaths)) return
|
||||||
windows.main.dispatch('onOpen', selectedPaths)
|
windows.main.dispatch('onOpen', selectedPaths)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Show open dialog to open a .torrent file.
|
* Show open dialog to open a .torrent file.
|
||||||
*/
|
*/
|
||||||
export function openTorrentFile () {
|
function openTorrentFile () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openTorrentFile')
|
log('openTorrentFile')
|
||||||
const opts = {
|
const opts = {
|
||||||
@@ -72,18 +80,19 @@ export function openTorrentFile () {
|
|||||||
properties: ['openFile', 'multiSelections']
|
properties: ['openFile', 'multiSelections']
|
||||||
}
|
}
|
||||||
setTitle(opts.title)
|
setTitle(opts.title)
|
||||||
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
|
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||||
resetTitle()
|
resetTitle()
|
||||||
if (!Array.isArray(selectedPaths)) return
|
if (!Array.isArray(selectedPaths)) return
|
||||||
selectedPaths.forEach(selectedPath => {
|
selectedPaths.forEach(function (selectedPath) {
|
||||||
windows.main.dispatch('addTorrent', selectedPath)
|
windows.main.dispatch('addTorrent', selectedPath)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.)
|
* Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.)
|
||||||
*/
|
*/
|
||||||
export function openTorrentAddress () {
|
function openTorrentAddress () {
|
||||||
log('openTorrentAddress')
|
log('openTorrentAddress')
|
||||||
windows.main.dispatch('openTorrentAddress')
|
windows.main.dispatch('openTorrentAddress')
|
||||||
}
|
}
|
||||||
@@ -107,8 +116,9 @@ function resetTitle () {
|
|||||||
*/
|
*/
|
||||||
function showOpenSeed (opts) {
|
function showOpenSeed (opts) {
|
||||||
setTitle(opts.title)
|
setTitle(opts.title)
|
||||||
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
|
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||||
resetTitle()
|
resetTitle()
|
||||||
if (!Array.isArray(selectedPaths)) return
|
if (!Array.isArray(selectedPaths)) return
|
||||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import electron from '../../electron.cjs'
|
module.exports = {
|
||||||
import * as dialog from './dialog.js'
|
downloadFinished,
|
||||||
import log from './log.js'
|
init,
|
||||||
|
setBadge
|
||||||
|
}
|
||||||
|
|
||||||
const { app, Menu } = electron
|
const { app, Menu } = require('electron')
|
||||||
|
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
const log = require('./log')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a right-click menu to the dock icon. (Mac)
|
* Add a right-click menu to the dock icon. (Mac)
|
||||||
@@ -29,7 +34,7 @@ function setBadge (count) {
|
|||||||
if (process.platform === 'darwin' ||
|
if (process.platform === 'darwin' ||
|
||||||
(process.platform === 'linux' && app.isUnityRunning())) {
|
(process.platform === 'linux' && app.isUnityRunning())) {
|
||||||
log(`setBadge: ${count}`)
|
log(`setBadge: ${count}`)
|
||||||
app.badgeCount = Number(count)
|
app.setBadgeCount(Number(count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,5 +57,3 @@ function getMenuTemplate () {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { downloadFinished, init, setBadge }
|
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
import cp from 'child_process'
|
module.exports = {
|
||||||
import path from 'path'
|
spawn,
|
||||||
import vlcCommand from 'vlc-command'
|
kill,
|
||||||
import log from './log.js'
|
checkInstall
|
||||||
import * as windows from './windows'
|
}
|
||||||
|
|
||||||
|
const cp = require('child_process')
|
||||||
|
const path = require('path')
|
||||||
|
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
|
// holds a ChildProcess while we're playing a video in an external player, null otherwise
|
||||||
let proc = null
|
let proc = null
|
||||||
|
|
||||||
export function checkInstall (playerPath, cb) {
|
function checkInstall (playerPath, cb) {
|
||||||
// check for VLC if external player has not been specified by the user
|
// check for VLC if external player has not been specified by the user
|
||||||
// otherwise assume the player is installed
|
// otherwise assume the player is installed
|
||||||
if (!playerPath) return vlcCommand(cb)
|
if (playerPath == null) return vlcCommand(cb)
|
||||||
process.nextTick(() => cb(null))
|
process.nextTick(() => cb(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawn (playerPath, url, title) {
|
function spawn (playerPath, url, title) {
|
||||||
if (playerPath) return spawnExternal(playerPath, [url])
|
if (playerPath != null) return spawnExternal(playerPath, [url])
|
||||||
|
|
||||||
// Try to find and use VLC if external player is not specified
|
// Try to find and use VLC if external player is not specified
|
||||||
vlcCommand((err, vlcPath) => {
|
vlcCommand((err, vlcPath) => {
|
||||||
@@ -29,7 +37,7 @@ export function spawn (playerPath, url, title) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function kill () {
|
function kill () {
|
||||||
if (!proc) return
|
if (!proc) return
|
||||||
log(`Killing external player, pid ${proc.pid}`)
|
log(`Killing external player, pid ${proc.pid}`)
|
||||||
proc.kill('SIGKILL') // kill -9
|
proc.kill('SIGKILL') // kill -9
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as chokidar from 'chokidar'
|
const chokidar = require('chokidar')
|
||||||
import log from './log.js'
|
const log = require('./log')
|
||||||
|
|
||||||
export class FolderWatcher {
|
class FolderWatcher {
|
||||||
constructor ({ window, state }) {
|
constructor ({ window, state }) {
|
||||||
this.window = window
|
this.window = window
|
||||||
this.state = state
|
this.state = state
|
||||||
@@ -46,3 +46,5 @@ export class FolderWatcher {
|
|||||||
this.watching = false
|
this.watching = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = FolderWatcher
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import config from '../config.js'
|
module.exports = {
|
||||||
import path from 'path'
|
install,
|
||||||
|
uninstall
|
||||||
|
}
|
||||||
|
|
||||||
export function install () {
|
const config = require('../config')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
function install () {
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case 'darwin': installDarwin()
|
case 'darwin': installDarwin()
|
||||||
break
|
break
|
||||||
@@ -10,7 +15,7 @@ export function install () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uninstall () {
|
function uninstall () {
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case 'darwin': uninstallDarwin()
|
case 'darwin': uninstallDarwin()
|
||||||
break
|
break
|
||||||
@@ -19,8 +24,10 @@ export function uninstall () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installDarwin () {
|
function installDarwin () {
|
||||||
const { app } = await import('electron')
|
const electron = require('electron')
|
||||||
|
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.
|
||||||
app.setAsDefaultProtocolClient('magnet')
|
app.setAsDefaultProtocolClient('magnet')
|
||||||
@@ -37,14 +44,33 @@ if (!config.IS_PRODUCTION) {
|
|||||||
EXEC_COMMAND.push(config.ROOT_PATH)
|
EXEC_COMMAND.push(config.ROOT_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installWin32 () {
|
function installWin32 () {
|
||||||
const Registry = await import('wingreg')
|
const Registry = require('winreg')
|
||||||
const log = await import('./log')
|
|
||||||
|
|
||||||
const iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
const log = require('./log')
|
||||||
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
|
|
||||||
registerProtocolHandlerWin32('stream-magnet', 'URL:BitTorrent Stream-Magnet URL', iconPath, EXEC_COMMAND)
|
const iconPath = path.join(
|
||||||
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND)
|
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
|
||||||
|
)
|
||||||
|
registerProtocolHandlerWin32(
|
||||||
|
'magnet',
|
||||||
|
'URL:BitTorrent Magnet URL',
|
||||||
|
iconPath,
|
||||||
|
EXEC_COMMAND
|
||||||
|
)
|
||||||
|
registerProtocolHandlerWin32(
|
||||||
|
'stream-magnet',
|
||||||
|
'URL:BitTorrent Stream-Magnet URL',
|
||||||
|
iconPath,
|
||||||
|
EXEC_COMMAND
|
||||||
|
)
|
||||||
|
registerFileHandlerWin32(
|
||||||
|
'.torrent',
|
||||||
|
'io.webtorrent.torrent',
|
||||||
|
'BitTorrent Document',
|
||||||
|
iconPath,
|
||||||
|
EXEC_COMMAND
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To add a protocol handler, the following keys must be added to the Windows registry:
|
* To add a protocol handler, the following keys must be added to the Windows registry:
|
||||||
@@ -172,8 +198,9 @@ async function installWin32 () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uninstallWin32 () {
|
function uninstallWin32 () {
|
||||||
const Registry = await import('winreg')
|
const Registry = require('winreg')
|
||||||
|
|
||||||
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
||||||
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
|
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
|
||||||
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
|
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
/* eslint-disable import/first */
|
|
||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
import * as RemoteMain from '@electron/remote/main/index.js'
|
const electron = require('electron')
|
||||||
RemoteMain.initialize()
|
const app = electron.app
|
||||||
import electron from '../../electron.cjs'
|
|
||||||
const { app, ipcMain } = electron
|
|
||||||
|
|
||||||
// Start crash reporter early, so it takes effect for child processes
|
const parallel = require('run-parallel')
|
||||||
import crashReporter from '../crash-reporter.js'
|
|
||||||
crashReporter.init()
|
|
||||||
|
|
||||||
import fs from 'fs'
|
const config = require('../config')
|
||||||
import parallel from 'run-parallel'
|
const crashReporter = require('../crash-reporter')
|
||||||
import config from '../config.js'
|
const ipc = require('./ipc')
|
||||||
import ipc from './ipc.js'
|
const log = require('./log')
|
||||||
import log from './log.js'
|
const menu = require('./menu')
|
||||||
import menu from './menu.js'
|
const State = require('../renderer/lib/state')
|
||||||
import State from '../renderer/lib/state.js'
|
const windows = require('./windows')
|
||||||
import * as windows from './windows/index.js'
|
|
||||||
|
|
||||||
const WEBTORRENT_VERSION = JSON.parse(fs.readFileSync('node_modules/webtorrent/package.json').toString()).version
|
const WEBTORRENT_VERSION = require('webtorrent/package.json').version
|
||||||
|
|
||||||
let shouldQuit = false
|
let shouldQuit = false
|
||||||
let argv = sliceArgv(process.argv)
|
let argv = sliceArgv(process.argv)
|
||||||
@@ -57,30 +51,27 @@ if (!shouldQuit && !config.IS_PORTABLE) {
|
|||||||
if (shouldQuit) {
|
if (shouldQuit) {
|
||||||
app.quit()
|
app.quit()
|
||||||
} else {
|
} else {
|
||||||
init().catch(console.error)
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init () {
|
function init () {
|
||||||
console.log('index init')
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
console.log('readyyyy')
|
|
||||||
})
|
|
||||||
app.on('second-instance', (event, commandLine, workingDirectory) => onAppOpen(commandLine))
|
app.on('second-instance', (event, commandLine, workingDirectory) => onAppOpen(commandLine))
|
||||||
if (config.IS_PORTABLE) {
|
if (config.IS_PORTABLE) {
|
||||||
console.log('is portable')
|
const path = require('path')
|
||||||
const path = await import('path')
|
|
||||||
// Put all user data into the "Portable Settings" folder
|
// Put all user data into the "Portable Settings" folder
|
||||||
app.setPath('userData', config.CONFIG_PATH)
|
app.setPath('userData', config.CONFIG_PATH)
|
||||||
// Put Electron crash files, etc. into the "Portable Settings\Temp" folder
|
// Put Electron crash files, etc. into the "Portable Settings\Temp" folder
|
||||||
app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp'))
|
app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ipcMain = electron.ipcMain
|
||||||
|
|
||||||
let 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
|
||||||
|
|
||||||
parallel({
|
parallel({
|
||||||
appReady: (cb) => app.whenReady().then(cb),
|
appReady: (cb) => app.on('ready', () => cb(null)),
|
||||||
state: (cb) => State.load(cb)
|
state: (cb) => State.load(cb)
|
||||||
}, onReady)
|
}, onReady)
|
||||||
|
|
||||||
@@ -90,9 +81,9 @@ async function init () {
|
|||||||
isReady = true
|
isReady = true
|
||||||
const state = results.state
|
const state = results.state
|
||||||
|
|
||||||
menu.init()
|
windows.main.init(state, { hidden: hidden })
|
||||||
windows.main.init(state, { hidden })
|
|
||||||
windows.webtorrent.init()
|
windows.webtorrent.init()
|
||||||
|
menu.init()
|
||||||
|
|
||||||
// To keep app startup fast, some code is delayed.
|
// To keep app startup fast, some code is delayed.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -118,13 +109,17 @@ async function init () {
|
|||||||
|
|
||||||
ipc.init()
|
ipc.init()
|
||||||
|
|
||||||
app.once('ipcReady', () => {
|
app.once('will-finish-launching', function () {
|
||||||
|
crashReporter.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.once('ipcReady', function () {
|
||||||
log('Command line args:', argv)
|
log('Command line args:', argv)
|
||||||
processArgv(argv)
|
processArgv(argv)
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('before-quit', e => {
|
app.on('before-quit', function (e) {
|
||||||
if (app.isQuitting) return
|
if (app.isQuitting) return
|
||||||
|
|
||||||
app.isQuitting = true
|
app.isQuitting = true
|
||||||
@@ -137,19 +132,18 @@ async function init () {
|
|||||||
}, 4000) // quit after 4 secs, at most
|
}, 4000) // quit after 4 secs, at most
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', function () {
|
||||||
console.log('activate')
|
|
||||||
if (isReady) windows.main.show()
|
if (isReady) windows.main.show()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function delayedInit (state) {
|
function delayedInit (state) {
|
||||||
if (app.isQuitting) return
|
if (app.isQuitting) return
|
||||||
|
|
||||||
const { default: announcement } = await import('./announcement.js')
|
const announcement = require('./announcement')
|
||||||
const { default: dock } = await import('./dock.js')
|
const dock = require('./dock')
|
||||||
const { default: updater } = await import('./updater.js')
|
const updater = require('./updater')
|
||||||
const { FolderWatcher } = await import('./folder-watcher.js')
|
const FolderWatcher = require('./folder-watcher')
|
||||||
const folderWatcher = new FolderWatcher({ window: windows.main, state })
|
const folderWatcher = new FolderWatcher({ window: windows.main, state })
|
||||||
|
|
||||||
announcement.init()
|
announcement.init()
|
||||||
@@ -162,13 +156,13 @@ async function delayedInit (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const userTasks = await import('./user-tasks.js')
|
const userTasks = require('./user-tasks')
|
||||||
userTasks.init()
|
userTasks.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
const { init: trayInit } = await import('./tray.js')
|
const tray = require('./tray')
|
||||||
trayInit()
|
tray.init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,21 +199,17 @@ function onAppOpen (newArgv) {
|
|||||||
// Development: 2 args, eg: electron .
|
// Development: 2 args, eg: electron .
|
||||||
// Test: 4 args, eg: electron -r .../mocks.js .
|
// Test: 4 args, eg: electron -r .../mocks.js .
|
||||||
function sliceArgv (argv) {
|
function sliceArgv (argv) {
|
||||||
return argv.slice(
|
return argv.slice(config.IS_PRODUCTION ? 1
|
||||||
config.IS_PRODUCTION
|
: config.IS_TEST ? 4
|
||||||
? 1
|
: 2)
|
||||||
: config.IS_TEST
|
|
||||||
? 4
|
|
||||||
: 2
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processArgv (argv) {
|
function processArgv (argv) {
|
||||||
const torrentIds = []
|
const torrentIds = []
|
||||||
await Promise.all(argv.forEach(async arg => {
|
argv.forEach(function (arg) {
|
||||||
if (arg === '-n' || arg === '-o' || arg === '-u') {
|
if (arg === '-n' || arg === '-o' || arg === '-u') {
|
||||||
// Critical path: Only load the 'dialog' package if it is needed
|
// Critical path: Only load the 'dialog' package if it is needed
|
||||||
const dialog = await import('./dialog')
|
const dialog = require('./dialog')
|
||||||
if (arg === '-n') {
|
if (arg === '-n') {
|
||||||
dialog.openSeedDirectory()
|
dialog.openSeedDirectory()
|
||||||
} else if (arg === '-o') {
|
} else if (arg === '-o') {
|
||||||
@@ -242,7 +232,7 @@ async function processArgv (argv) {
|
|||||||
// running.
|
// running.
|
||||||
torrentIds.push(arg)
|
torrentIds.push(arg)
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
if (torrentIds.length > 0) {
|
if (torrentIds.length > 0) {
|
||||||
windows.main.dispatch('onOpen', torrentIds)
|
windows.main.dispatch('onOpen', torrentIds)
|
||||||
}
|
}
|
||||||
|
|||||||
149
src/main/ipc.js
149
src/main/ipc.js
@@ -1,10 +1,15 @@
|
|||||||
import electron from '../../electron.cjs'
|
module.exports = {
|
||||||
|
init,
|
||||||
|
setModule
|
||||||
|
}
|
||||||
|
|
||||||
import log from './log.js'
|
const electron = require('electron')
|
||||||
import menu from './menu.js'
|
|
||||||
import * as windows from './windows/index.js'
|
|
||||||
|
|
||||||
const { app, ipcMain } = electron
|
const app = electron.app
|
||||||
|
|
||||||
|
const log = require('./log')
|
||||||
|
const menu = require('./menu')
|
||||||
|
const windows = require('./windows')
|
||||||
|
|
||||||
// Messages from the main process, to be sent once the WebTorrent process starts
|
// Messages from the main process, to be sent once the WebTorrent process starts
|
||||||
const messageQueueMainToWebTorrent = []
|
const messageQueueMainToWebTorrent = []
|
||||||
@@ -18,16 +23,18 @@ function setModule (name, module) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
ipcMain.once('ipcReady', e => {
|
const ipc = electron.ipcMain
|
||||||
|
|
||||||
|
ipc.once('ipcReady', function (e) {
|
||||||
app.ipcReady = true
|
app.ipcReady = true
|
||||||
app.emit('ipcReady')
|
app.emit('ipcReady')
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.once('ipcReadyWebTorrent', e => {
|
ipc.once('ipcReadyWebTorrent', function (e) {
|
||||||
app.ipcReadyWebTorrent = true
|
app.ipcReadyWebTorrent = true
|
||||||
log('sending %d queued messages from the main win to the webtorrent window',
|
log('sending %d queued messages from the main win to the webtorrent window',
|
||||||
messageQueueMainToWebTorrent.length)
|
messageQueueMainToWebTorrent.length)
|
||||||
messageQueueMainToWebTorrent.forEach(message => {
|
messageQueueMainToWebTorrent.forEach(function (message) {
|
||||||
windows.webtorrent.send(message.name, ...message.args)
|
windows.webtorrent.send(message.name, ...message.args)
|
||||||
log('webtorrent: sent queued %s', message.name)
|
log('webtorrent: sent queued %s', message.name)
|
||||||
})
|
})
|
||||||
@@ -37,12 +44,12 @@ function init () {
|
|||||||
* Dialog
|
* Dialog
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipcMain.on('openTorrentFile', async () => {
|
ipc.on('openTorrentFile', () => {
|
||||||
const dialog = await import('./dialog')
|
const dialog = require('./dialog')
|
||||||
dialog.openTorrentFile()
|
dialog.openTorrentFile()
|
||||||
})
|
})
|
||||||
ipcMain.on('openFiles', async () => {
|
ipc.on('openFiles', () => {
|
||||||
const dialog = await import('./dialog')
|
const dialog = require('./dialog')
|
||||||
dialog.openFiles()
|
dialog.openFiles()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -50,12 +57,12 @@ function init () {
|
|||||||
* Dock
|
* Dock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipcMain.on('setBadge', async (e, ...args) => {
|
ipc.on('setBadge', (e, ...args) => {
|
||||||
const dock = await import('./dock')
|
const dock = require('./dock')
|
||||||
dock.setBadge(...args)
|
dock.setBadge(...args)
|
||||||
})
|
})
|
||||||
ipcMain.on('downloadFinished', async (e, ...args) => {
|
ipc.on('downloadFinished', (e, ...args) => {
|
||||||
const dock = await import('./dock')
|
const dock = require('./dock')
|
||||||
dock.downloadFinished(...args)
|
dock.downloadFinished(...args)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -63,10 +70,10 @@ function init () {
|
|||||||
* Player Events
|
* Player Events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipcMain.on('onPlayerOpen', async () => {
|
ipc.on('onPlayerOpen', function () {
|
||||||
const powerSaveBlocker = await import('./power-save-blocker')
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
const shortcuts = await import('./shortcuts')
|
const shortcuts = require('./shortcuts')
|
||||||
const thumbar = await import('./thumbar')
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
menu.togglePlaybackControls(true)
|
menu.togglePlaybackControls(true)
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
@@ -74,17 +81,17 @@ function init () {
|
|||||||
thumbar.enable()
|
thumbar.enable()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('onPlayerUpdate', async (e, ...args) => {
|
ipc.on('onPlayerUpdate', function (e, ...args) {
|
||||||
const thumbar = await import('./thumbar')
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
menu.onPlayerUpdate(...args)
|
menu.onPlayerUpdate(...args)
|
||||||
thumbar.onPlayerUpdate(...args)
|
thumbar.onPlayerUpdate(...args)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('onPlayerClose', async () => {
|
ipc.on('onPlayerClose', function () {
|
||||||
const powerSaveBlocker = await import('./power-save-blocker')
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
const shortcuts = await import('./shortcuts')
|
const shortcuts = require('./shortcuts')
|
||||||
const thumbar = await import('./thumbar')
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
menu.togglePlaybackControls(false)
|
menu.togglePlaybackControls(false)
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
@@ -92,17 +99,17 @@ function init () {
|
|||||||
thumbar.disable()
|
thumbar.disable()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('onPlayerPlay', async () => {
|
ipc.on('onPlayerPlay', function () {
|
||||||
const powerSaveBlocker = await import('./power-save-blocker')
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
const thumbar = await import('./thumbar')
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
thumbar.onPlayerPlay()
|
thumbar.onPlayerPlay()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('onPlayerPause', async () => {
|
ipc.on('onPlayerPause', function () {
|
||||||
const powerSaveBlocker = await import('./power-save-blocker')
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
const thumbar = await import('./thumbar')
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
thumbar.onPlayerPause()
|
thumbar.onPlayerPause()
|
||||||
@@ -112,7 +119,7 @@ function init () {
|
|||||||
* Folder Watcher Events
|
* Folder Watcher Events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipcMain.on('startFolderWatcher', () => {
|
ipc.on('startFolderWatcher', function () {
|
||||||
if (!modules.folderWatcher) {
|
if (!modules.folderWatcher) {
|
||||||
log('IPC ERR: folderWatcher module is not defined.')
|
log('IPC ERR: folderWatcher module is not defined.')
|
||||||
return
|
return
|
||||||
@@ -121,7 +128,7 @@ function init () {
|
|||||||
modules.folderWatcher.start()
|
modules.folderWatcher.start()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('stopFolderWatcher', () => {
|
ipc.on('stopFolderWatcher', function () {
|
||||||
if (!modules.folderWatcher) {
|
if (!modules.folderWatcher) {
|
||||||
log('IPC ERR: folderWatcher module is not defined.')
|
log('IPC ERR: folderWatcher module is not defined.')
|
||||||
return
|
return
|
||||||
@@ -134,16 +141,16 @@ function init () {
|
|||||||
* Shell
|
* Shell
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipcMain.on('openPath', async (e, ...args) => {
|
ipc.on('openItem', (e, ...args) => {
|
||||||
const shell = await import('./shell')
|
const shell = require('./shell')
|
||||||
shell.openPath(...args)
|
shell.openItem(...args)
|
||||||
})
|
})
|
||||||
ipcMain.on('showItemInFolder', async (e, ...args) => {
|
ipc.on('showItemInFolder', (e, ...args) => {
|
||||||
const shell = await import('./shell')
|
const shell = require('./shell')
|
||||||
shell.showItemInFolder(...args)
|
shell.showItemInFolder(...args)
|
||||||
})
|
})
|
||||||
ipcMain.on('moveItemToTrash', async (e, ...args) => {
|
ipc.on('moveItemToTrash', (e, ...args) => {
|
||||||
const shell = await import('./shell')
|
const shell = require('./shell')
|
||||||
shell.moveItemToTrash(...args)
|
shell.moveItemToTrash(...args)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -151,8 +158,8 @@ function init () {
|
|||||||
* File handlers
|
* File handlers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipcMain.on('setDefaultFileHandler', async (e, flag) => {
|
ipc.on('setDefaultFileHandler', (e, flag) => {
|
||||||
const handlers = await import('./handlers')
|
const handlers = require('./handlers')
|
||||||
|
|
||||||
if (flag) handlers.install()
|
if (flag) handlers.install()
|
||||||
else handlers.uninstall()
|
else handlers.uninstall()
|
||||||
@@ -162,8 +169,8 @@ function init () {
|
|||||||
* Auto start on login
|
* Auto start on login
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipcMain.on('setStartup', async (e, flag) => {
|
ipc.on('setStartup', (e, flag) => {
|
||||||
const startup = await import('./startup')
|
const startup = require('./startup')
|
||||||
|
|
||||||
if (flag) startup.install()
|
if (flag) startup.install()
|
||||||
else startup.uninstall()
|
else startup.uninstall()
|
||||||
@@ -175,39 +182,37 @@ function init () {
|
|||||||
|
|
||||||
const main = windows.main
|
const main = windows.main
|
||||||
|
|
||||||
ipcMain.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
|
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
|
||||||
ipcMain.on('setBounds', (e, ...args) => main.setBounds(...args))
|
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
|
||||||
ipcMain.on('setProgress', (e, ...args) => main.setProgress(...args))
|
ipc.on('setProgress', (e, ...args) => main.setProgress(...args))
|
||||||
ipcMain.on('setTitle', (e, ...args) => main.setTitle(...args))
|
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
|
||||||
ipcMain.on('show', () => main.show())
|
ipc.on('show', () => main.show())
|
||||||
ipcMain.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
|
||||||
ipcMain.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
|
ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* External Media Player
|
* External Media Player
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipcMain.on('checkForExternalPlayer', async (e, path) => {
|
ipc.on('checkForExternalPlayer', function (e, path) {
|
||||||
const externalPlayer = await import('./external-player')
|
const externalPlayer = require('./external-player')
|
||||||
|
|
||||||
externalPlayer.checkInstall(path, err => {
|
externalPlayer.checkInstall(path, function (err) {
|
||||||
windows.main.send('checkForExternalPlayer', !err)
|
windows.main.send('checkForExternalPlayer', !err)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('openExternalPlayer', async (e, ...args) => {
|
ipc.on('openExternalPlayer', (e, ...args) => {
|
||||||
const externalPlayer = await import('./external-player')
|
const externalPlayer = require('./external-player')
|
||||||
const shortcuts = await import('./shortcuts')
|
const thumbar = require('./thumbar')
|
||||||
const thumbar = await import('./thumbar')
|
|
||||||
|
|
||||||
menu.togglePlaybackControls(false)
|
menu.togglePlaybackControls(false)
|
||||||
shortcuts.disable()
|
|
||||||
thumbar.disable()
|
thumbar.disable()
|
||||||
externalPlayer.spawn(...args)
|
externalPlayer.spawn(...args)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('quitExternalPlayer', async () => {
|
ipc.on('quitExternalPlayer', () => {
|
||||||
const externalPlayer = await import('./external-player')
|
const externalPlayer = require('./external-player')
|
||||||
externalPlayer.kill()
|
externalPlayer.kill()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -215,12 +220,11 @@ function init () {
|
|||||||
* Message passing
|
* Message passing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const oldEmit = ipcMain.emit
|
const oldEmit = ipc.emit
|
||||||
ipcMain.emit = (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) {
|
||||||
console.dir(e.sender.getTitle())
|
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
|
||||||
if (e.sender.getTitle() === 'WebTorrent Hidden Window') {
|
|
||||||
// Send message to main window
|
// Send message to main window
|
||||||
windows.main.send(name, ...args)
|
windows.main.send(name, ...args)
|
||||||
log('webtorrent: got %s', name)
|
log('webtorrent: got %s', name)
|
||||||
@@ -231,8 +235,8 @@ function init () {
|
|||||||
} else {
|
} else {
|
||||||
// Queue message for webtorrent window, it hasn't finished loading yet
|
// Queue message for webtorrent window, it hasn't finished loading yet
|
||||||
messageQueueMainToWebTorrent.push({
|
messageQueueMainToWebTorrent.push({
|
||||||
name,
|
name: name,
|
||||||
args
|
args: args
|
||||||
})
|
})
|
||||||
log('webtorrent: queueing %s', name)
|
log('webtorrent: queueing %s', name)
|
||||||
}
|
}
|
||||||
@@ -240,13 +244,6 @@ function init () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Emit all other events normally
|
// Emit all other events normally
|
||||||
oldEmit.call(ipcMain, name, e, ...args)
|
oldEmit.call(ipc, name, e, ...args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { init }
|
|
||||||
export { setModule }
|
|
||||||
export default {
|
|
||||||
init,
|
|
||||||
setModule
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import electron from '../../electron.cjs'
|
module.exports = log
|
||||||
import * as windows from './windows/index.js'
|
module.exports.error = error
|
||||||
|
|
||||||
const { app } = electron
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the main electron process, we do not use console.log() statements because they do
|
* In the main electron process, we do not use console.log() statements because they do
|
||||||
@@ -9,7 +7,13 @@ const { app } = electron
|
|||||||
* version of the app. Instead use this module, which sends the logs to the main window
|
* version of the app. Instead use this module, which sends the logs to the main window
|
||||||
* where they can be viewed in Developer Tools.
|
* where they can be viewed in Developer Tools.
|
||||||
*/
|
*/
|
||||||
export default function log (...args) {
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const windows = require('./windows')
|
||||||
|
|
||||||
|
const app = electron.app
|
||||||
|
|
||||||
|
function log (...args) {
|
||||||
if (app.ipcReady) {
|
if (app.ipcReady) {
|
||||||
windows.main.send('log', ...args)
|
windows.main.send('log', ...args)
|
||||||
} else {
|
} else {
|
||||||
@@ -17,7 +21,7 @@ export default function log (...args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function error (...args) {
|
function error (...args) {
|
||||||
if (app.ipcReady) {
|
if (app.ipcReady) {
|
||||||
windows.main.send('error', ...args)
|
windows.main.send('error', ...args)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import electron from '../../electron.cjs'
|
module.exports = {
|
||||||
import config from '../config.js'
|
init,
|
||||||
import * as windows from './windows/index.js'
|
togglePlaybackControls,
|
||||||
import shell from './shell.js'
|
setWindowFocus,
|
||||||
|
setAllowNav,
|
||||||
|
onPlayerUpdate,
|
||||||
|
onToggleAlwaysOnTop,
|
||||||
|
onToggleFullScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const app = electron.app
|
||||||
|
|
||||||
|
const config = require('../config')
|
||||||
|
const windows = require('./windows')
|
||||||
|
|
||||||
let menu = null
|
let menu = null
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
console.log('menu init')
|
|
||||||
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||||
electron.Menu.setApplicationMenu(menu)
|
electron.Menu.setApplicationMenu(menu)
|
||||||
}
|
}
|
||||||
@@ -59,11 +70,12 @@ function onToggleFullScreen (flag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMenuItem (label) {
|
function getMenuItem (label) {
|
||||||
for (const menuItem of menu.items) {
|
for (let i = 0; i < menu.items.length; i++) {
|
||||||
const submenuItem = menuItem.submenu.items.find(item => item.label === label)
|
const menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||||
if (submenuItem) return submenuItem
|
return item.label === label
|
||||||
|
})
|
||||||
|
if (menuItem) return menuItem
|
||||||
}
|
}
|
||||||
return {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMenuTemplate () {
|
function getMenuTemplate () {
|
||||||
@@ -76,24 +88,24 @@ function getMenuTemplate () {
|
|||||||
? 'Create New Torrent...'
|
? 'Create New Torrent...'
|
||||||
: 'Create New Torrent from Folder...',
|
: 'Create New Torrent from Folder...',
|
||||||
accelerator: 'CmdOrCtrl+N',
|
accelerator: 'CmdOrCtrl+N',
|
||||||
click: async () => {
|
click: () => {
|
||||||
const dialog = await import('./dialog')
|
const dialog = require('./dialog')
|
||||||
dialog.openSeedDirectory()
|
dialog.openSeedDirectory()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent File...',
|
label: 'Open Torrent File...',
|
||||||
accelerator: 'CmdOrCtrl+O',
|
accelerator: 'CmdOrCtrl+O',
|
||||||
click: async () => {
|
click: () => {
|
||||||
const dialog = await import('./dialog')
|
const dialog = require('./dialog')
|
||||||
dialog.openTorrentFile()
|
dialog.openTorrentFile()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent Address...',
|
label: 'Open Torrent Address...',
|
||||||
accelerator: 'CmdOrCtrl+U',
|
accelerator: 'CmdOrCtrl+U',
|
||||||
click: async () => {
|
click: () => {
|
||||||
const dialog = await import('./dialog')
|
const dialog = require('./dialog')
|
||||||
dialog.openTorrentAddress()
|
dialog.openTorrentAddress()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -276,14 +288,6 @@ function getMenuTemplate () {
|
|||||||
{
|
{
|
||||||
label: 'Resume All',
|
label: 'Resume All',
|
||||||
click: () => windows.main.dispatch('resumeAllTorrents')
|
click: () => windows.main.dispatch('resumeAllTorrents')
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Remove All From List',
|
|
||||||
click: () => windows.main.dispatch('confirmDeleteAllTorrents', false)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Remove All Data Files',
|
|
||||||
click: () => windows.main.dispatch('confirmDeleteAllTorrents', true)
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -294,18 +298,21 @@ function getMenuTemplate () {
|
|||||||
{
|
{
|
||||||
label: 'Learn more about ' + config.APP_NAME,
|
label: 'Learn more about ' + config.APP_NAME,
|
||||||
click: () => {
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
shell.openExternal(config.HOME_PAGE_URL)
|
shell.openExternal(config.HOME_PAGE_URL)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Release Notes',
|
label: 'Release Notes',
|
||||||
click: () => {
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
shell.openExternal(config.GITHUB_URL_RELEASES)
|
shell.openExternal(config.GITHUB_URL_RELEASES)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Contribute on GitHub',
|
label: 'Contribute on GitHub',
|
||||||
click: () => {
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
shell.openExternal(config.GITHUB_URL)
|
shell.openExternal(config.GITHUB_URL)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -315,18 +322,21 @@ function getMenuTemplate () {
|
|||||||
{
|
{
|
||||||
label: 'Report an Issue...',
|
label: 'Report an Issue...',
|
||||||
click: () => {
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
shell.openExternal(config.GITHUB_URL_ISSUES)
|
shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Follow us on Twitter',
|
label: 'Follow us on Twitter',
|
||||||
click: () => {
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
shell.openExternal(config.TWITTER_PAGE_URL)
|
shell.openExternal(config.TWITTER_PAGE_URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
// WebTorrent menu (Mac)
|
// WebTorrent menu (Mac)
|
||||||
template.unshift({
|
template.unshift({
|
||||||
@@ -347,7 +357,8 @@ function getMenuTemplate () {
|
|||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'services'
|
role: 'services',
|
||||||
|
submenu: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
@@ -411,8 +422,8 @@ function getMenuTemplate () {
|
|||||||
// File menu (Windows, Linux)
|
// File menu (Windows, Linux)
|
||||||
template[0].submenu.unshift({
|
template[0].submenu.unshift({
|
||||||
label: 'Create New Torrent from File...',
|
label: 'Create New Torrent from File...',
|
||||||
click: async () => {
|
click: () => {
|
||||||
const dialog = await import('./dialog')
|
const dialog = require('./dialog')
|
||||||
dialog.openSeedFile()
|
dialog.openSeedFile()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -445,26 +456,9 @@ function getMenuTemplate () {
|
|||||||
// File menu (Linux)
|
// File menu (Linux)
|
||||||
template[0].submenu.push({
|
template[0].submenu.push({
|
||||||
label: 'Quit',
|
label: 'Quit',
|
||||||
click: () => electron.app.quit()
|
click: () => app.quit()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
|
||||||
export { init }
|
|
||||||
export { togglePlaybackControls }
|
|
||||||
export { setWindowFocus }
|
|
||||||
export { setAllowNav }
|
|
||||||
export { onPlayerUpdate }
|
|
||||||
export { onToggleAlwaysOnTop }
|
|
||||||
export { onToggleFullScreen }
|
|
||||||
export default {
|
|
||||||
init,
|
|
||||||
togglePlaybackControls,
|
|
||||||
setWindowFocus,
|
|
||||||
setAllowNav,
|
|
||||||
onPlayerUpdate,
|
|
||||||
onToggleAlwaysOnTop,
|
|
||||||
onToggleFullScreen
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { powerSaveBlocker } from 'electron'
|
module.exports = {
|
||||||
import log from './log.js'
|
enable,
|
||||||
|
disable
|
||||||
|
}
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const log = require('./log')
|
||||||
|
|
||||||
let blockId = 0
|
let blockId = 0
|
||||||
|
|
||||||
@@ -7,23 +12,23 @@ 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.
|
||||||
*/
|
*/
|
||||||
export function enable () {
|
function enable () {
|
||||||
if (powerSaveBlocker.isStarted(blockId)) {
|
if (electron.powerSaveBlocker.isStarted(blockId)) {
|
||||||
// If a power saver block already exists, do nothing.
|
// If a power saver block already exists, do nothing.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
blockId = powerSaveBlocker.start('prevent-display-sleep')
|
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
|
||||||
log(`powerSaveBlocker.enable: ${blockId}`)
|
log(`powerSaveBlocker.enable: ${blockId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop blocking the system from entering low-power mode.
|
* Stop blocking the system from entering low-power mode.
|
||||||
*/
|
*/
|
||||||
export function disable () {
|
function disable () {
|
||||||
if (!powerSaveBlocker.isStarted(blockId)) {
|
if (!electron.powerSaveBlocker.isStarted(blockId)) {
|
||||||
// If a power saver block does not exist, do nothing.
|
// If a power saver block does not exist, do nothing.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
powerSaveBlocker.stop(blockId)
|
electron.powerSaveBlocker.stop(blockId)
|
||||||
log(`powerSaveBlocker.disable: ${blockId}`)
|
log(`powerSaveBlocker.disable: ${blockId}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
import electron from '../../electron.cjs'
|
module.exports = {
|
||||||
import log from './log.js'
|
openExternal,
|
||||||
|
openItem,
|
||||||
|
showItemInFolder,
|
||||||
|
moveItemToTrash
|
||||||
|
}
|
||||||
|
|
||||||
const { shell } = electron
|
const electron = require('electron')
|
||||||
|
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.
|
||||||
*/
|
*/
|
||||||
function openExternal (url) {
|
function openExternal (url) {
|
||||||
log(`openExternal: ${url}`)
|
log(`openExternal: ${url}`)
|
||||||
shell.openExternal(url)
|
electron.shell.openExternal(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the given file in the desktop’s default manner.
|
* Open the given file in the desktop’s default manner.
|
||||||
*/
|
*/
|
||||||
function openPath (path) {
|
function openItem (path) {
|
||||||
log(`openPath: ${path}`)
|
log(`openItem: ${path}`)
|
||||||
shell.openPath(path)
|
electron.shell.openItem(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +29,7 @@ function openPath (path) {
|
|||||||
*/
|
*/
|
||||||
function showItemInFolder (path) {
|
function showItemInFolder (path) {
|
||||||
log(`showItemInFolder: ${path}`)
|
log(`showItemInFolder: ${path}`)
|
||||||
shell.showItemInFolder(path)
|
electron.shell.showItemInFolder(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,12 +37,5 @@ function showItemInFolder (path) {
|
|||||||
*/
|
*/
|
||||||
function moveItemToTrash (path) {
|
function moveItemToTrash (path) {
|
||||||
log(`moveItemToTrash: ${path}`)
|
log(`moveItemToTrash: ${path}`)
|
||||||
shell.trashItem(path)
|
electron.shell.moveItemToTrash(path)
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
openExternal,
|
|
||||||
openPath,
|
|
||||||
showItemInFolder,
|
|
||||||
moveItemToTrash
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
import { globalShortcut } from 'electron'
|
module.exports = {
|
||||||
import * as windows from './windows'
|
disable,
|
||||||
|
enable
|
||||||
|
}
|
||||||
|
|
||||||
export function enable () {
|
const electron = require('electron')
|
||||||
|
const windows = require('./windows')
|
||||||
|
|
||||||
|
function enable () {
|
||||||
// Register play/pause media key, available on some keyboards.
|
// Register play/pause media key, available on some keyboards.
|
||||||
globalShortcut.register(
|
electron.globalShortcut.register(
|
||||||
'MediaPlayPause',
|
'MediaPlayPause',
|
||||||
() => windows.main.dispatch('playPause')
|
() => windows.main.dispatch('playPause')
|
||||||
)
|
)
|
||||||
globalShortcut.register(
|
electron.globalShortcut.register(
|
||||||
'MediaNextTrack',
|
'MediaNextTrack',
|
||||||
() => windows.main.dispatch('nextTrack')
|
() => windows.main.dispatch('nextTrack')
|
||||||
)
|
)
|
||||||
globalShortcut.register(
|
electron.globalShortcut.register(
|
||||||
'MediaPreviousTrack',
|
'MediaPreviousTrack',
|
||||||
() => windows.main.dispatch('previousTrack')
|
() => windows.main.dispatch('previousTrack')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export 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.
|
||||||
globalShortcut.unregister('MediaPlayPause')
|
electron.globalShortcut.unregister('MediaPlayPause')
|
||||||
globalShortcut.unregister('MediaNextTrack')
|
electron.globalShortcut.unregister('MediaNextTrack')
|
||||||
globalShortcut.unregister('MediaPreviousTrack')
|
electron.globalShortcut.unregister('MediaPreviousTrack')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,151 @@
|
|||||||
import { app } from 'electron'
|
module.exports = {
|
||||||
import path from 'path'
|
handleEvent
|
||||||
import { spawn } from 'child_process'
|
}
|
||||||
import handlers from './handlers.js'
|
|
||||||
|
const cp = require('child_process')
|
||||||
|
const electron = require('electron')
|
||||||
|
const fs = require('fs')
|
||||||
|
const os = require('os')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const app = electron.app
|
||||||
|
|
||||||
|
const handlers = require('./handlers')
|
||||||
|
|
||||||
const EXE_NAME = path.basename(process.execPath)
|
const EXE_NAME = path.basename(process.execPath)
|
||||||
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||||
|
|
||||||
const run = (args, done) => {
|
function handleEvent (cmd) {
|
||||||
spawn(UPDATE_EXE, args, { detached: true }).on('close', done)
|
if (cmd === '--squirrel-install') {
|
||||||
}
|
// App was installed. Install desktop/start menu shortcuts.
|
||||||
|
createShortcuts(function () {
|
||||||
|
// Ensure user sees install splash screen so they realize that Setup.exe actually
|
||||||
|
// installed an application and isn't the application itself.
|
||||||
|
setTimeout(function () {
|
||||||
|
app.quit()
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
export function handleEvent (cmd) {
|
if (cmd === '--squirrel-updated') {
|
||||||
if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') {
|
// App was updated. (Called on new version of app)
|
||||||
run([`--createShortcut=${EXE_NAME}`], app.quit)
|
updateShortcuts(function () {
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === '--squirrel-uninstall') {
|
if (cmd === '--squirrel-uninstall') {
|
||||||
|
// App was just uninstalled. Undo anything we did in the --squirrel-install and
|
||||||
|
// --squirrel-updated handlers
|
||||||
|
|
||||||
// Uninstall .torrent file and magnet link handlers
|
// Uninstall .torrent file and magnet link handlers
|
||||||
handlers.uninstall()
|
handlers.uninstall()
|
||||||
|
|
||||||
run([`--removeShortcut=${EXE_NAME}`], app.quit)
|
// Remove desktop/start menu shortcuts.
|
||||||
|
// HACK: add a callback to handlers.uninstall() so we can remove this setTimeout
|
||||||
|
setTimeout(function () {
|
||||||
|
removeShortcuts(function () {
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === '--squirrel-obsolete') {
|
if (cmd === '--squirrel-obsolete') {
|
||||||
|
// App will be updated. (Called on outgoing version of app)
|
||||||
app.quit()
|
app.quit()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cmd === '--squirrel-firstrun') {
|
||||||
|
// App is running for the first time. Do not quit, allow startup to continue.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a command and invoke the callback when it completes with an error and
|
||||||
|
* the output from standard out.
|
||||||
|
*/
|
||||||
|
function spawn (command, args, cb) {
|
||||||
|
let stdout = ''
|
||||||
|
let error = null
|
||||||
|
let child = null
|
||||||
|
try {
|
||||||
|
child = cp.spawn(command, args)
|
||||||
|
} catch (err) {
|
||||||
|
// Spawn can throw an error
|
||||||
|
process.nextTick(function () {
|
||||||
|
cb(error, stdout)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
child.stdout.on('data', function (data) {
|
||||||
|
stdout += data
|
||||||
|
})
|
||||||
|
|
||||||
|
child.on('error', function (processError) {
|
||||||
|
error = processError
|
||||||
|
})
|
||||||
|
|
||||||
|
child.on('close', function (code, signal) {
|
||||||
|
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
|
||||||
|
if (error) error.stdout = stdout
|
||||||
|
cb(error, stdout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn the Squirrel `Update.exe` command with the given arguments and invoke
|
||||||
|
* the callback when the command completes.
|
||||||
|
*/
|
||||||
|
function spawnUpdate (args, cb) {
|
||||||
|
spawn(UPDATE_EXE, args, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create desktop and start menu shortcuts using the Squirrel `Update.exe`
|
||||||
|
* command.
|
||||||
|
*/
|
||||||
|
function createShortcuts (cb) {
|
||||||
|
spawnUpdate(['--createShortcut', EXE_NAME], cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update desktop and start menu shortcuts using the Squirrel `Update.exe`
|
||||||
|
* command.
|
||||||
|
*/
|
||||||
|
function updateShortcuts (cb) {
|
||||||
|
const homeDir = os.homedir()
|
||||||
|
if (homeDir) {
|
||||||
|
const desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
||||||
|
// If the desktop shortcut was deleted by the user, then keep it deleted.
|
||||||
|
fs.access(desktopShortcutPath, function (err) {
|
||||||
|
const desktopShortcutExists = !err
|
||||||
|
createShortcuts(function () {
|
||||||
|
if (desktopShortcutExists) {
|
||||||
|
cb()
|
||||||
|
} else {
|
||||||
|
// Remove the unwanted desktop shortcut that was recreated
|
||||||
|
fs.unlink(desktopShortcutPath, cb)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
createShortcuts(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove desktop and start menu shortcuts using the Squirrel `Update.exe`
|
||||||
|
* command.
|
||||||
|
*/
|
||||||
|
function removeShortcuts (cb) {
|
||||||
|
spawnUpdate(['--removeShortcut', EXE_NAME], cb)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { APP_NAME } from '../config.js'
|
module.exports = {
|
||||||
import AutoLaunch from 'auto-launch'
|
install,
|
||||||
|
uninstall
|
||||||
|
}
|
||||||
|
|
||||||
|
const { APP_NAME } = require('../config')
|
||||||
|
const AutoLaunch = require('auto-launch')
|
||||||
|
|
||||||
const appLauncher = new AutoLaunch({
|
const appLauncher = new AutoLaunch({
|
||||||
name: APP_NAME,
|
name: APP_NAME,
|
||||||
isHidden: true
|
isHidden: true
|
||||||
})
|
})
|
||||||
|
|
||||||
export function install () {
|
function install () {
|
||||||
return appLauncher
|
return appLauncher
|
||||||
.isEnabled()
|
.isEnabled()
|
||||||
.then(enabled => {
|
.then(enabled => {
|
||||||
@@ -14,7 +19,7 @@ export function install () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uninstall () {
|
function uninstall () {
|
||||||
return appLauncher
|
return appLauncher
|
||||||
.isEnabled()
|
.isEnabled()
|
||||||
.then(enabled => {
|
.then(enabled => {
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
import path from 'path'
|
module.exports = {
|
||||||
import config from '../config.js'
|
disable,
|
||||||
import * as windows from './windows'
|
enable,
|
||||||
|
onPlayerPause,
|
||||||
|
onPlayerPlay,
|
||||||
|
onPlayerUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On Windows, add a "thumbnail toolbar" with a play/pause button in the taskbar.
|
||||||
|
* This provides users a way to access play/pause functionality without restoring
|
||||||
|
* or activating the window.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
const config = require('../config')
|
||||||
|
|
||||||
|
const windows = require('./windows')
|
||||||
|
|
||||||
const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png')
|
const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png')
|
||||||
const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
|
const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
|
||||||
@@ -17,7 +32,7 @@ let buttons = []
|
|||||||
/**
|
/**
|
||||||
* Show the Windows thumbnail toolbar buttons.
|
* Show the Windows thumbnail toolbar buttons.
|
||||||
*/
|
*/
|
||||||
export function enable () {
|
function enable () {
|
||||||
buttons = [
|
buttons = [
|
||||||
{
|
{
|
||||||
tooltip: 'Previous Track',
|
tooltip: 'Previous Track',
|
||||||
@@ -41,26 +56,26 @@ export function enable () {
|
|||||||
/**
|
/**
|
||||||
* Hide the Windows thumbnail toolbar buttons.
|
* Hide the Windows thumbnail toolbar buttons.
|
||||||
*/
|
*/
|
||||||
export function disable () {
|
function disable () {
|
||||||
buttons = []
|
buttons = []
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onPlayerPause () {
|
function onPlayerPause () {
|
||||||
if (!isEnabled()) return
|
if (!isEnabled()) return
|
||||||
buttons[PLAY_PAUSE].tooltip = 'Play'
|
buttons[PLAY_PAUSE].tooltip = 'Play'
|
||||||
buttons[PLAY_PAUSE].icon = PLAY_ICON
|
buttons[PLAY_PAUSE].icon = PLAY_ICON
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onPlayerPlay () {
|
function onPlayerPlay () {
|
||||||
if (!isEnabled()) return
|
if (!isEnabled()) return
|
||||||
buttons[PLAY_PAUSE].tooltip = 'Pause'
|
buttons[PLAY_PAUSE].tooltip = 'Pause'
|
||||||
buttons[PLAY_PAUSE].icon = PAUSE_ICON
|
buttons[PLAY_PAUSE].icon = PAUSE_ICON
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onPlayerUpdate (state) {
|
function onPlayerUpdate (state) {
|
||||||
if (!isEnabled()) return
|
if (!isEnabled()) return
|
||||||
buttons[PREV].flags = [state.hasPrevious ? 'enabled' : 'disabled']
|
buttons[PREV].flags = [state.hasPrevious ? 'enabled' : 'disabled']
|
||||||
buttons[NEXT].flags = [state.hasNext ? 'enabled' : 'disabled']
|
buttons[NEXT].flags = [state.hasNext ? 'enabled' : 'disabled']
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { app, Tray, Menu } from 'electron'
|
module.exports = {
|
||||||
import config from '../config.js'
|
hasTray,
|
||||||
import * as windows from './windows'
|
init,
|
||||||
|
setWindowFocus
|
||||||
|
}
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const app = electron.app
|
||||||
|
|
||||||
|
const config = require('../config')
|
||||||
|
const windows = require('./windows')
|
||||||
|
|
||||||
let tray
|
let tray
|
||||||
|
|
||||||
export function init () {
|
function init () {
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
initLinux()
|
initLinux()
|
||||||
}
|
}
|
||||||
@@ -17,17 +26,17 @@ export function init () {
|
|||||||
/**
|
/**
|
||||||
* Returns true if there a tray icon is active.
|
* Returns true if there a tray icon is active.
|
||||||
*/
|
*/
|
||||||
export function hasTray () {
|
function hasTray () {
|
||||||
return !!tray
|
return !!tray
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setWindowFocus (flag) {
|
function setWindowFocus (flag) {
|
||||||
if (!tray) return
|
if (!tray) return
|
||||||
updateTrayMenu()
|
updateTrayMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
function initLinux () {
|
function initLinux () {
|
||||||
checkLinuxTraySupport(err => {
|
checkLinuxTraySupport(function (err) {
|
||||||
if (!err) createTray()
|
if (!err) createTray()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -37,20 +46,28 @@ function initWin32 () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for libappindicator support before creating tray icon.
|
* Check for libappindicator1 support before creating tray icon
|
||||||
*/
|
*/
|
||||||
async function checkLinuxTraySupport (cb) {
|
function checkLinuxTraySupport (cb) {
|
||||||
const cp = await import('child_process')
|
const cp = require('child_process')
|
||||||
|
|
||||||
// Check that libappindicator libraries are installed in system.
|
// Check that we're on Ubuntu (or another debian system) and that we have
|
||||||
cp.exec('ldconfig -p | grep libappindicator', (err, stdout) => {
|
// libappindicator1. If WebTorrent was installed from the deb file, we should
|
||||||
|
// always have it. If it was installed from the zip file, we might not.
|
||||||
|
cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
cb(null)
|
// Unfortunately there's no cleaner way, as far as I can tell, to check
|
||||||
|
// whether a debian package is installed:
|
||||||
|
if (stdout.endsWith('\tinstall\n')) {
|
||||||
|
cb(null)
|
||||||
|
} else {
|
||||||
|
cb(new Error('debian package not installed'))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTray () {
|
function createTray () {
|
||||||
tray = new Tray(getIconPath())
|
tray = new electron.Tray(getIconPath())
|
||||||
|
|
||||||
// On Windows, left click opens the app, right click opens the context menu.
|
// On Windows, left click opens the app, right click opens the context menu.
|
||||||
// On Linux, any click (left or right) opens the context menu.
|
// On Linux, any click (left or right) opens the context menu.
|
||||||
@@ -61,7 +78,7 @@ function createTray () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateTrayMenu () {
|
function updateTrayMenu () {
|
||||||
const contextMenu = Menu.buildFromTemplate(getMenuTemplate())
|
const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||||
tray.setContextMenu(contextMenu)
|
tray.setContextMenu(contextMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import electron from '../../electron.cjs'
|
module.exports = {
|
||||||
import get from 'simple-get'
|
init
|
||||||
import config from '../config.js'
|
}
|
||||||
import log from './log.js'
|
|
||||||
import * as windows from './windows/index.js'
|
|
||||||
|
|
||||||
const { autoUpdater } = electron
|
const electron = require('electron')
|
||||||
|
const get = require('simple-get')
|
||||||
|
|
||||||
|
const config = require('../config')
|
||||||
|
const log = require('./log')
|
||||||
|
const windows = require('./windows')
|
||||||
|
|
||||||
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||||
'?version=' + config.APP_VERSION +
|
'?version=' + config.APP_VERSION +
|
||||||
@@ -19,8 +22,6 @@ function init () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { init }
|
|
||||||
|
|
||||||
// The Electron auto-updater does not support Linux yet, so manually check for
|
// The Electron auto-updater does not support Linux yet, so manually check for
|
||||||
// updates and show the user a modal notification.
|
// updates and show the user a modal notification.
|
||||||
function initLinux () {
|
function initLinux () {
|
||||||
@@ -46,31 +47,31 @@ function onResponse (err, res, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initDarwinWin32 () {
|
function initDarwinWin32 () {
|
||||||
autoUpdater.on(
|
electron.autoUpdater.on(
|
||||||
'error',
|
'error',
|
||||||
(err) => log(`Update error: ${err.message}`)
|
(err) => log.error(`Update error: ${err.message}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
autoUpdater.on(
|
electron.autoUpdater.on(
|
||||||
'checking-for-update',
|
'checking-for-update',
|
||||||
() => log('Checking for update')
|
() => log('Checking for update')
|
||||||
)
|
)
|
||||||
|
|
||||||
autoUpdater.on(
|
electron.autoUpdater.on(
|
||||||
'update-available',
|
'update-available',
|
||||||
() => log('Update available')
|
() => log('Update available')
|
||||||
)
|
)
|
||||||
|
|
||||||
autoUpdater.on(
|
electron.autoUpdater.on(
|
||||||
'update-not-available',
|
'update-not-available',
|
||||||
() => log('No update available')
|
() => log('No update available')
|
||||||
)
|
)
|
||||||
|
|
||||||
autoUpdater.on(
|
electron.autoUpdater.on(
|
||||||
'update-downloaded',
|
'update-downloaded',
|
||||||
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
|
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
autoUpdater.setFeedURL({ url: AUTO_UPDATE_URL })
|
electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
|
||||||
autoUpdater.checkForUpdates()
|
electron.autoUpdater.checkForUpdates()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { app } from 'electron'
|
module.exports = {
|
||||||
|
init
|
||||||
|
}
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
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)
|
||||||
*/
|
*/
|
||||||
export function init () {
|
function init () {
|
||||||
if (process.platform !== 'win32') return
|
if (process.platform !== 'win32') return
|
||||||
app.setUserTasks(getUserTasks())
|
app.setUserTasks(getUserTasks())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import config from '../../config.js'
|
const about = module.exports = {
|
||||||
import electron from '../../../electron.cjs'
|
|
||||||
import * as RemoteMain from '@electron/remote/main/index.js'
|
|
||||||
|
|
||||||
export const about = {
|
|
||||||
init,
|
init,
|
||||||
win: null
|
win: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
if (about.win) {
|
if (about.win) {
|
||||||
return about.win.show()
|
return about.win.show()
|
||||||
@@ -16,7 +15,7 @@ function init () {
|
|||||||
backgroundColor: '#ECECEC',
|
backgroundColor: '#ECECEC',
|
||||||
center: true,
|
center: true,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
height: 250,
|
height: 170,
|
||||||
icon: getIconPath(),
|
icon: getIconPath(),
|
||||||
maximizable: false,
|
maximizable: false,
|
||||||
minimizable: false,
|
minimizable: false,
|
||||||
@@ -26,26 +25,21 @@ function init () {
|
|||||||
title: 'About ' + config.APP_WINDOW_TITLE,
|
title: 'About ' + config.APP_WINDOW_TITLE,
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true
|
||||||
contextIsolation: false,
|
|
||||||
enableBlinkFeatures: 'AudioVideoTracks',
|
|
||||||
enableRemoteModule: true,
|
|
||||||
backgroundThrottling: false
|
|
||||||
},
|
},
|
||||||
width: 300
|
width: 300
|
||||||
})
|
})
|
||||||
RemoteMain.enable(win.webContents)
|
|
||||||
win.loadURL(config.WINDOW_ABOUT)
|
win.loadURL(config.WINDOW_ABOUT)
|
||||||
|
|
||||||
win.once('ready-to-show', () => {
|
// No menu on the About window
|
||||||
|
win.setMenu(null)
|
||||||
|
|
||||||
|
win.once('ready-to-show', function () {
|
||||||
win.show()
|
win.show()
|
||||||
// No menu on the About window
|
|
||||||
// Hack: BrowserWindow removeMenu method not working on electron@7
|
|
||||||
// https://github.com/electron/electron/issues/21088
|
|
||||||
win.setMenuBarVisibility(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
win.once('closed', () => {
|
win.once('closed', function () {
|
||||||
about.win = null
|
about.win = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './about.js'
|
exports.about = require('./about')
|
||||||
export * from './main.js'
|
exports.main = require('./main')
|
||||||
export * from './webtorrent.js'
|
exports.webtorrent = require('./webtorrent')
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
import debounce from 'debounce'
|
const main = module.exports = {
|
||||||
import electron from '../../../electron.cjs'
|
|
||||||
import config from '../../config.js'
|
|
||||||
import log from '../log.js'
|
|
||||||
import menu from '../menu.js'
|
|
||||||
import * as RemoteMain from '@electron/remote/main/index.js'
|
|
||||||
|
|
||||||
const { app, BrowserWindow, screen } = electron
|
|
||||||
|
|
||||||
export const main = {
|
|
||||||
dispatch,
|
dispatch,
|
||||||
hide,
|
hide,
|
||||||
init,
|
init,
|
||||||
@@ -23,6 +14,15 @@ export const main = {
|
|||||||
win: null
|
win: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const debounce = require('debounce')
|
||||||
|
|
||||||
|
const app = electron.app
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
const log = require('../log')
|
||||||
|
const menu = require('../menu')
|
||||||
|
|
||||||
function init (state, options) {
|
function init (state, options) {
|
||||||
if (main.win) {
|
if (main.win) {
|
||||||
return main.win.show()
|
return main.win.show()
|
||||||
@@ -30,8 +30,9 @@ function init (state, options) {
|
|||||||
|
|
||||||
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
|
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
|
||||||
|
|
||||||
const win = main.win = new BrowserWindow({
|
const win = main.win = new electron.BrowserWindow({
|
||||||
backgroundColor: '#282828',
|
backgroundColor: '#282828',
|
||||||
|
backgroundThrottling: false, // do not throttle animations/timers when page is background
|
||||||
darkTheme: true, // Forces dark theme (GTK+3)
|
darkTheme: true, // Forces dark theme (GTK+3)
|
||||||
height: initialBounds.height,
|
height: initialBounds.height,
|
||||||
icon: getIconPath(), // Window icon (Windows, Linux)
|
icon: getIconPath(), // Window icon (Windows, Linux)
|
||||||
@@ -43,17 +44,12 @@ function init (state, options) {
|
|||||||
useContentSize: true, // Specify web page size without OS chrome
|
useContentSize: true, // Specify web page size without OS chrome
|
||||||
width: initialBounds.width,
|
width: initialBounds.width,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true
|
||||||
contextIsolation: false,
|
|
||||||
enableBlinkFeatures: 'AudioVideoTracks',
|
|
||||||
enableRemoteModule: true,
|
|
||||||
backgroundThrottling: false
|
|
||||||
},
|
},
|
||||||
x: initialBounds.x,
|
x: initialBounds.x,
|
||||||
y: initialBounds.y
|
y: initialBounds.y
|
||||||
})
|
})
|
||||||
RemoteMain.enable(win.webContents)
|
|
||||||
console.log(config.WINDOW_MAIN)
|
|
||||||
win.loadURL(config.WINDOW_MAIN)
|
win.loadURL(config.WINDOW_MAIN)
|
||||||
|
|
||||||
win.once('ready-to-show', () => {
|
win.once('ready-to-show', () => {
|
||||||
@@ -68,7 +64,7 @@ function init (state, options) {
|
|||||||
menu.onToggleFullScreen(main.win.isFullScreen())
|
menu.onToggleFullScreen(main.win.isFullScreen())
|
||||||
})
|
})
|
||||||
|
|
||||||
win.webContents.on('will-navigate', (e) => {
|
win.webContents.on('will-navigate', (e, url) => {
|
||||||
// Prevent drag-and-drop from navigating the Electron window, which can happen
|
// Prevent drag-and-drop from navigating the Electron window, which can happen
|
||||||
// before our drag-and-drop handlers have been initialized.
|
// before our drag-and-drop handlers have been initialized.
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -100,10 +96,10 @@ function init (state, options) {
|
|||||||
send('windowBoundsChanged', e.sender.getBounds())
|
send('windowBoundsChanged', e.sender.getBounds())
|
||||||
}, 1000))
|
}, 1000))
|
||||||
|
|
||||||
win.on('close', async e => {
|
win.on('close', e => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
const { hasTray } = await import('../tray.js')
|
const tray = require('../tray')
|
||||||
if (!hasTray()) {
|
if (!tray.hasTray()) {
|
||||||
app.quit()
|
app.quit()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -154,7 +150,7 @@ function setBounds (bounds, maximize) {
|
|||||||
log('setBounds: maximizing')
|
log('setBounds: maximizing')
|
||||||
main.win.maximize()
|
main.win.maximize()
|
||||||
} else if (maximize === false && main.win.isMaximized()) {
|
} else if (maximize === false && main.win.isMaximized()) {
|
||||||
log('setBounds: minimizing')
|
log('setBounds: unmaximizing')
|
||||||
main.win.unmaximize()
|
main.win.unmaximize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +160,7 @@ function setBounds (bounds, maximize) {
|
|||||||
log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`)
|
log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`)
|
||||||
if (bounds.x === null && bounds.y === null) {
|
if (bounds.x === null && bounds.y === null) {
|
||||||
// X and Y not specified? By default, center on current screen
|
// X and Y not specified? By default, center on current screen
|
||||||
const scr = 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)}`)
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import electron from '../../../electron.cjs'
|
const webtorrent = module.exports = {
|
||||||
import config from '../../config.js'
|
|
||||||
import * as RemoteMain from '@electron/remote/main/index.js'
|
|
||||||
|
|
||||||
export const webtorrent = {
|
|
||||||
init,
|
init,
|
||||||
send,
|
send,
|
||||||
show,
|
show,
|
||||||
@@ -10,9 +6,14 @@ export const webtorrent = {
|
|||||||
win: null
|
win: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
const win = webtorrent.win = new electron.BrowserWindow({
|
const win = webtorrent.win = new electron.BrowserWindow({
|
||||||
backgroundColor: '#1E1E1E',
|
backgroundColor: '#1E1E1E',
|
||||||
|
backgroundThrottling: false, // do not throttle animations/timers when page is background
|
||||||
center: true,
|
center: true,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
fullscreenable: false,
|
fullscreenable: false,
|
||||||
@@ -25,18 +26,15 @@ function init () {
|
|||||||
title: 'webtorrent-hidden-window',
|
title: 'webtorrent-hidden-window',
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true
|
||||||
contextIsolation: false,
|
|
||||||
enableBlinkFeatures: 'AudioVideoTracks',
|
|
||||||
enableRemoteModule: true,
|
|
||||||
backgroundThrottling: false
|
|
||||||
},
|
},
|
||||||
width: 150
|
width: 150
|
||||||
})
|
})
|
||||||
RemoteMain.enable(win.webContents)
|
|
||||||
win.loadURL(config.WINDOW_WEBTORRENT)
|
win.loadURL(config.WINDOW_WEBTORRENT)
|
||||||
|
|
||||||
// Prevent killing the WebTorrent process
|
// Prevent killing the WebTorrent process
|
||||||
win.on('close', e => {
|
win.on('close', function (e) {
|
||||||
if (electron.app.isQuitting) {
|
if (electron.app.isQuitting) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import { dispatcher } from '../lib/dispatcher.js'
|
|
||||||
|
|
||||||
export default class CreateTorrentErrorPage extends React.Component {
|
const { dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class CreateTorrentErrorPage extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className='create-torrent'>
|
<div className='create-torrent'>
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
import ModalOKCancel from './modal-ok-cancel.js'
|
|
||||||
import { dispatch, dispatcher } from '../lib/dispatcher.js'
|
|
||||||
|
|
||||||
export default class DeleteAllTorrentsModal extends React.Component {
|
|
||||||
render () {
|
|
||||||
const { state: { modal: { deleteData } } } = this.props
|
|
||||||
const message = deleteData
|
|
||||||
? 'Are you sure you want to remove all the torrents from the list and delete the data files?'
|
|
||||||
: 'Are you sure you want to remove all the torrents from the list?'
|
|
||||||
const buttonText = deleteData ? 'REMOVE DATA' : 'REMOVE'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p><strong>{message}</strong></p>
|
|
||||||
<ModalOKCancel
|
|
||||||
cancelText='CANCEL'
|
|
||||||
onCancel={dispatcher('exitModal')}
|
|
||||||
okText={buttonText}
|
|
||||||
onOK={handleRemove}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
function handleRemove () {
|
|
||||||
dispatch('deleteAllTorrents', deleteData)
|
|
||||||
dispatch('exitModal')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import { dispatcher } from '../lib/dispatcher.js'
|
|
||||||
|
|
||||||
export default class Header extends React.Component {
|
const { dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
class Header extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const loc = this.props.state.location
|
const loc = this.props.state.location
|
||||||
return (
|
return (
|
||||||
@@ -55,3 +56,5 @@ export default class Header extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = Header
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import PropTypes from 'prop-types'
|
const PropTypes = require('prop-types')
|
||||||
import * as colors from 'material-ui/styles/colors'
|
|
||||||
|
|
||||||
export default class Heading extends React.Component {
|
const colors = require('material-ui/styles/colors')
|
||||||
|
|
||||||
|
class Heading extends React.Component {
|
||||||
static get propTypes () {
|
static get propTypes () {
|
||||||
return {
|
return {
|
||||||
level: PropTypes.number
|
level: PropTypes.number
|
||||||
@@ -30,3 +31,5 @@ export default class Heading extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = Heading
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import flatButton from 'material-ui/FlatButton'
|
const FlatButton = require('material-ui/FlatButton').default
|
||||||
import raisedButton from 'material-ui/RaisedButton'
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
|
||||||
const FlatButton = flatButton.default
|
module.exports = class ModalOKCancel extends React.Component {
|
||||||
const RaisedButton = raisedButton.default
|
|
||||||
export default class ModalOKCancel extends React.Component {
|
|
||||||
render () {
|
render () {
|
||||||
const cancelStyle = { marginRight: 10, color: 'black' }
|
const cancelStyle = { marginRight: 10, color: 'black' }
|
||||||
const { cancelText, onCancel, okText, onOK } = this.props
|
const { cancelText, onCancel, okText, onOK } = this.props
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import textField from 'material-ui/TextField'
|
const TextField = require('material-ui/TextField').default
|
||||||
import electron from 'electron'
|
const { clipboard } = require('electron')
|
||||||
import ModalOKCancel from './modal-ok-cancel.js'
|
|
||||||
import { dispatch, dispatcher } from '../lib/dispatcher.js'
|
|
||||||
import { isMagnetLink } from '../lib/torrent-player.js'
|
|
||||||
|
|
||||||
const TextField = textField.default
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
const { clipboard } = electron
|
const { dispatch, dispatcher } = require('../lib/dispatcher')
|
||||||
|
const { isMagnetLink } = require('../lib/torrent-player')
|
||||||
|
|
||||||
export default class OpenTorrentAddressModal extends React.Component {
|
module.exports = class OpenTorrentAddressModal extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className='open-torrent-address-modal'>
|
<div className='open-torrent-address-modal'>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import path from 'path'
|
const path = require('path')
|
||||||
import * as colors from 'material-ui/styles/colors'
|
|
||||||
import remote from '@electron/remote'
|
const colors = require('material-ui/styles/colors')
|
||||||
import * as React from 'react'
|
const electron = require('electron')
|
||||||
import PropTypes from 'prop-types'
|
const React = require('react')
|
||||||
import raisedButton from 'material-ui/RaisedButton'
|
const PropTypes = require('prop-types')
|
||||||
import textField from 'material-ui/TextField'
|
|
||||||
|
const remote = electron.remote
|
||||||
|
|
||||||
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
const TextField = require('material-ui/TextField').default
|
||||||
|
|
||||||
const RaisedButton = raisedButton.default
|
|
||||||
const TextField = textField.default
|
|
||||||
// Lets you pick a file or directory.
|
// Lets you pick a file or directory.
|
||||||
// Uses the system Open File dialog.
|
// Uses the system Open File dialog.
|
||||||
// You can't edit the text field directly.
|
// You can't edit the text field directly.
|
||||||
export default class PathSelector extends React.Component {
|
class PathSelector extends React.Component {
|
||||||
static propTypes () {
|
static propTypes () {
|
||||||
return {
|
return {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
@@ -30,13 +32,18 @@ export default class PathSelector extends React.Component {
|
|||||||
|
|
||||||
handleClick () {
|
handleClick () {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
defaultPath: path.dirname(this.props.value || ''),
|
defaultPath: this.props.value && path.dirname(this.props.value),
|
||||||
properties: ['openFile', 'openDirectory']
|
properties: ['openFile', 'openDirectory']
|
||||||
}, this.props.dialog)
|
}, this.props.dialog)
|
||||||
|
|
||||||
const filenames = remote.dialog.showOpenDialogSync(remote.getCurrentWindow(), opts)
|
remote.dialog.showOpenDialog(
|
||||||
if (!Array.isArray(filenames)) return
|
remote.getCurrentWindow(),
|
||||||
this.props.onChange && this.props.onChange(filenames[0])
|
opts,
|
||||||
|
(filenames) => {
|
||||||
|
if (!Array.isArray(filenames)) return
|
||||||
|
this.props.onChange && this.props.onChange(filenames[0])
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -81,3 +88,5 @@ export default class PathSelector extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = PathSelector
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import ModalOKCancel from './modal-ok-cancel.js'
|
|
||||||
import { dispatch, dispatcher } from '../lib/dispatcher.js'
|
|
||||||
|
|
||||||
export default class RemoveTorrentModal extends React.Component {
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
|
const { dispatch, dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class RemoveTorrentModal extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const state = this.props.state
|
const state = this.props.state
|
||||||
const message = state.modal.deleteData
|
const message = state.modal.deleteData
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import PropTypes from 'prop-types'
|
const PropTypes = require('prop-types')
|
||||||
import raisedButton from 'material-ui/RaisedButton'
|
|
||||||
|
|
||||||
const RaisedButton = raisedButton.default
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
|
||||||
export default class ShowMore extends React.Component {
|
class ShowMore extends React.Component {
|
||||||
static get propTypes () {
|
static get propTypes () {
|
||||||
return {
|
return {
|
||||||
defaultExpanded: PropTypes.bool,
|
defaultExpanded: PropTypes.bool,
|
||||||
@@ -22,9 +21,11 @@ export default class ShowMore extends React.Component {
|
|||||||
|
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
expanded: !!this.props.defaultExpanded
|
expanded: !!this.props.defaultExpanded
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleClick = this.handleClick.bind(this)
|
this.handleClick = this.handleClick.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,3 +51,5 @@ export default class ShowMore extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = ShowMore
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import { shell } from 'electron'
|
const electron = require('electron')
|
||||||
import ModalOKCancel from './modal-ok-cancel.js'
|
|
||||||
import { dispatcher } from '../lib/dispatcher.js'
|
|
||||||
|
|
||||||
export default class UnsupportedMediaModal extends React.Component {
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
|
const { dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class UnsupportedMediaModal extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const state = this.props.state
|
const state = this.props.state
|
||||||
const err = state.modal.error
|
const err = state.modal.error
|
||||||
@@ -35,7 +36,7 @@ export default class UnsupportedMediaModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onInstall () {
|
onInstall () {
|
||||||
shell.openExternal('https://www.videolan.org/vlc/')
|
electron.shell.openExternal('http://www.videolan.org/vlc/')
|
||||||
|
|
||||||
// TODO: dcposch send a dispatch rather than modifying state directly
|
// TODO: dcposch send a dispatch rather than modifying state directly
|
||||||
const state = this.props.state
|
const state = this.props.state
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import { shell } from 'electron'
|
const electron = require('electron')
|
||||||
import ModalOKCancel from './modal-ok-cancel.js'
|
|
||||||
import { dispatch } from '../lib/dispatcher.js'
|
|
||||||
|
|
||||||
export default class UpdateAvailableModal extends React.Component {
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class UpdateAvailableModal extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const state = this.props.state
|
const state = this.props.state
|
||||||
return (
|
return (
|
||||||
@@ -24,7 +25,7 @@ export default class UpdateAvailableModal extends React.Component {
|
|||||||
|
|
||||||
function handleShow () {
|
function handleShow () {
|
||||||
// TODO: use the GitHub urls from config.js
|
// TODO: use the GitHub urls from config.js
|
||||||
shell.openExternal('https://github.com/webtorrent/webtorrent-desktop/releases')
|
electron.shell.openExternal('https://github.com/webtorrent/webtorrent-desktop/releases')
|
||||||
dispatch('exitModal')
|
dispatch('exitModal')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { dispatch } from '../lib/dispatcher.js'
|
|
||||||
|
|
||||||
export default class AudioTracksController {
|
|
||||||
constructor (state) {
|
|
||||||
this.state = state
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAudioTrack (ix) {
|
|
||||||
this.state.playing.audioTracks.selectedIndex = ix
|
|
||||||
dispatch('skip', 0.2) // HACK: hardcoded seek value for smooth audio change
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleAudioTracksMenu () {
|
|
||||||
const audioTracks = this.state.playing.audioTracks
|
|
||||||
audioTracks.showMenu = !audioTracks.showMenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ipcRenderer } from 'electron'
|
const { ipcRenderer } = require('electron')
|
||||||
|
|
||||||
export default class FolderWatcherController {
|
module.exports = class FolderWatcherController {
|
||||||
start () {
|
start () {
|
||||||
console.log('-- IPC: start folder watcher')
|
console.log('-- IPC: start folder watcher')
|
||||||
ipcRenderer.send('startFolderWatcher')
|
ipcRenderer.send('startFolderWatcher')
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { ipcRenderer } from 'electron'
|
const { ipcRenderer } = require('electron')
|
||||||
import telemetry from '../lib/telemetry.js'
|
const telemetry = require('../lib/telemetry')
|
||||||
import Playlist from '../lib/playlist.js'
|
const Playlist = require('../lib/playlist')
|
||||||
|
|
||||||
export default class MediaController {
|
// Controls local play back: the <video>/<audio> tag and VLC
|
||||||
|
// Does not control remote casting (Chromecast etc)
|
||||||
|
module.exports = class MediaController {
|
||||||
constructor (state) {
|
constructor (state) {
|
||||||
this.state = state
|
this.state = state
|
||||||
}
|
}
|
||||||
@@ -21,10 +23,10 @@ export default class MediaController {
|
|||||||
telemetry.logPlayAttempt('error')
|
telemetry.logPlayAttempt('error')
|
||||||
state.playing.location = 'error'
|
state.playing.location = 'error'
|
||||||
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
|
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
|
||||||
ipcRenderer.once('checkForExternalPlayer', (e, isInstalled) => {
|
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
|
||||||
state.modal = {
|
state.modal = {
|
||||||
id: 'unsupported-media-modal',
|
id: 'unsupported-media-modal',
|
||||||
error,
|
error: error,
|
||||||
externalPlayerInstalled: isInstalled
|
externalPlayerInstalled: isInstalled
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -54,7 +56,7 @@ export default class MediaController {
|
|||||||
const state = this.state
|
const state = this.state
|
||||||
state.playing.location = 'external'
|
state.playing.location = 'external'
|
||||||
|
|
||||||
const onServerRunning = () => {
|
const onServerRunning = function () {
|
||||||
state.playing.isReady = true
|
state.playing.isReady = true
|
||||||
telemetry.logPlayAttempt('external')
|
telemetry.logPlayAttempt('external')
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,21 @@
|
|||||||
import { ipcRenderer } from 'electron'
|
const electron = require('electron')
|
||||||
import path from 'path'
|
const path = require('path')
|
||||||
import Cast from '../lib/cast.js'
|
|
||||||
import { dispatch } from '../lib/dispatcher.js'
|
const Cast = require('../lib/cast')
|
||||||
import telemetry from '../lib/telemetry.js'
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
import { UnplayableFileError, UnplayableTorrentError } from '../lib/errors.js'
|
const telemetry = require('../lib/telemetry')
|
||||||
import sound from '../lib/sound.js'
|
const { UnplayableFileError, UnplayableTorrentError } = require('../lib/errors')
|
||||||
import TorrentPlayer from '../lib/torrent-player.js'
|
const sound = require('../lib/sound')
|
||||||
import TorrentSummary from '../lib/torrent-summary.js'
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
import Playlist from '../lib/playlist.js'
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
import State from '../lib/state.js'
|
const Playlist = require('../lib/playlist')
|
||||||
// Checks whether we are connected and already casting
|
const State = require('../lib/state')
|
||||||
// Returns false if we not casting (state.playing.location === 'local')
|
|
||||||
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
|
const ipcRenderer = electron.ipcRenderer
|
||||||
function isCasting (state) {
|
|
||||||
return state.playing.location === 'chromecast' ||
|
// Controls playback of torrents and files within torrents
|
||||||
state.playing.location === 'airplay' ||
|
// both local (<video>,<audio>,external player) and remote (cast)
|
||||||
state.playing.location === 'dlna'
|
module.exports = class PlaybackController {
|
||||||
}
|
|
||||||
function restoreBounds (state) {
|
|
||||||
ipcRenderer.send('setAspectRatio', 0)
|
|
||||||
if (state.window.bounds) {
|
|
||||||
ipcRenderer.send('setBounds', state.window.bounds, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default class PlaybackController {
|
|
||||||
constructor (state, config, update) {
|
constructor (state, config, update) {
|
||||||
this.state = state
|
this.state = state
|
||||||
this.config = config
|
this.config = config
|
||||||
@@ -36,10 +28,12 @@ export default class PlaybackController {
|
|||||||
// * If no file index is provided, restore the most recently viewed file or autoplay the first
|
// * 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.pauseActiveTorrents(infoHash)
|
this.pauseActiveTorrents(infoHash)
|
||||||
|
|
||||||
const state = this.state
|
const state = this.state
|
||||||
if (state.location.url() === 'player') {
|
if (state.location.url() === 'player') {
|
||||||
this.updatePlayer(infoHash, index, false, (err) => {
|
this.updatePlayer(infoHash, index, false, (err) => {
|
||||||
if (err) { dispatch('error', err) } else { this.play() }
|
if (err) dispatch('error', err)
|
||||||
|
else this.play()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let initialized = false
|
let initialized = false
|
||||||
@@ -47,10 +41,13 @@ export default class PlaybackController {
|
|||||||
url: 'player',
|
url: 'player',
|
||||||
setup: (cb) => {
|
setup: (cb) => {
|
||||||
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||||
|
|
||||||
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
|
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
|
||||||
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
|
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
|
||||||
if (index === undefined) return cb(new UnplayableTorrentError())
|
if (index === undefined) return cb(new UnplayableTorrentError())
|
||||||
|
|
||||||
initialized = true
|
initialized = true
|
||||||
|
|
||||||
this.openPlayer(infoHash, index, (err) => {
|
this.openPlayer(infoHash, index, (err) => {
|
||||||
if (!err) this.play()
|
if (!err) this.play()
|
||||||
cb(err)
|
cb(err)
|
||||||
@@ -58,37 +55,45 @@ export default class PlaybackController {
|
|||||||
},
|
},
|
||||||
destroy: () => this.closePlayer()
|
destroy: () => this.closePlayer()
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
if (err) { dispatch('error', err) }
|
if (err) dispatch('error', err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open a file in OS default app.
|
// Open a file in OS default app.
|
||||||
openPath (infoHash, index) {
|
openItem (infoHash, index) {
|
||||||
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
const filePath = path.join(torrentSummary.path, torrentSummary.files[index].path)
|
const filePath = path.join(
|
||||||
ipcRenderer.send('openPath', filePath)
|
torrentSummary.path,
|
||||||
|
torrentSummary.files[index].path)
|
||||||
|
ipcRenderer.send('openItem', filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle (play or pause) the currently playing media
|
// Toggle (play or pause) the currently playing media
|
||||||
playPause () {
|
playPause () {
|
||||||
const 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
|
||||||
const 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() } else { mediaTag.pause() }
|
if (state.playing.isPaused) mediaTag.play()
|
||||||
|
else mediaTag.pause()
|
||||||
}
|
}
|
||||||
if (state.playing.isPaused) { this.play() } else { this.pause() }
|
|
||||||
|
if (state.playing.isPaused) this.play()
|
||||||
|
else this.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
pauseActiveTorrents (infoHash) {
|
pauseActiveTorrents (infoHash) {
|
||||||
// Playback Priority: pause all active torrents if needed.
|
// Playback Priority: pause all active torrents if needed.
|
||||||
if (!this.state.saved.prefs.highestPlaybackPriority) return
|
if (!this.state.saved.prefs.highestPlaybackPriority) return
|
||||||
|
|
||||||
// Do not pause active torrents if playing a fully downloaded torrent.
|
// Do not pause active torrents if playing a fully downloaded torrent.
|
||||||
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
if (torrentSummary.status === 'seeding') return
|
if (torrentSummary.status === 'seeding') return
|
||||||
|
|
||||||
dispatch('prioritizeTorrent', infoHash)
|
dispatch('prioritizeTorrent', infoHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,9 +101,11 @@ export default class PlaybackController {
|
|||||||
nextTrack () {
|
nextTrack () {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
|
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
|
||||||
this.updatePlayer(state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
this.updatePlayer(
|
||||||
if (err) { dispatch('error', err) } else { this.play() }
|
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
||||||
})
|
if (err) dispatch('error', err)
|
||||||
|
else this.play()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,9 +113,11 @@ export default class PlaybackController {
|
|||||||
previousTrack () {
|
previousTrack () {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
|
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
|
||||||
this.updatePlayer(state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
this.updatePlayer(
|
||||||
if (err) { dispatch('error', err) } else { this.play() }
|
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
||||||
})
|
if (err) dispatch('error', err)
|
||||||
|
else this.play()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,21 +154,8 @@ export default class PlaybackController {
|
|||||||
console.error('Tried to skip to a non-finite time ' + time)
|
console.error('Tried to skip to a non-finite time ' + time)
|
||||||
return console.trace()
|
return console.trace()
|
||||||
}
|
}
|
||||||
if (isCasting(this.state)) { Cast.seek(time) } else { this.state.playing.jumpToTime = time }
|
if (isCasting(this.state)) Cast.seek(time)
|
||||||
}
|
else this.state.playing.jumpToTime = time
|
||||||
|
|
||||||
// Show video preview
|
|
||||||
preview (x) {
|
|
||||||
if (!Number.isFinite(x)) {
|
|
||||||
console.error('Tried to preview a non-finite position ' + x)
|
|
||||||
return console.trace()
|
|
||||||
}
|
|
||||||
this.state.playing.previewXCoord = x
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide video preview
|
|
||||||
clearPreview () {
|
|
||||||
this.state.playing.previewXCoord = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change playback speed. 1 = faster, -1 = slower
|
// Change playback speed. 1 = faster, -1 = slower
|
||||||
@@ -197,6 +193,7 @@ export default class PlaybackController {
|
|||||||
setVolume (volume) {
|
setVolume (volume) {
|
||||||
// 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))
|
||||||
|
|
||||||
const state = this.state
|
const state = this.state
|
||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
Cast.setVolume(volume)
|
Cast.setVolume(volume)
|
||||||
@@ -213,6 +210,7 @@ export default class PlaybackController {
|
|||||||
showOrHidePlayerControls () {
|
showOrHidePlayerControls () {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
const hideControls = state.shouldHidePlayerControls()
|
const hideControls = state.shouldHidePlayerControls()
|
||||||
|
|
||||||
if (hideControls !== state.playing.hideControls) {
|
if (hideControls !== state.playing.hideControls) {
|
||||||
state.playing.hideControls = hideControls
|
state.playing.hideControls = hideControls
|
||||||
return true
|
return true
|
||||||
@@ -224,10 +222,13 @@ export default class PlaybackController {
|
|||||||
openPlayer (infoHash, index, cb) {
|
openPlayer (infoHash, index, cb) {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||||
|
|
||||||
state.playing.infoHash = torrentSummary.infoHash
|
state.playing.infoHash = torrentSummary.infoHash
|
||||||
state.playing.isReady = false
|
state.playing.isReady = false
|
||||||
|
|
||||||
// update UI to show pending playback
|
// update UI to show pending playback
|
||||||
sound.play('PLAY')
|
sound.play('PLAY')
|
||||||
|
|
||||||
this.startServer(torrentSummary)
|
this.startServer(torrentSummary)
|
||||||
ipcRenderer.send('onPlayerOpen')
|
ipcRenderer.send('onPlayerOpen')
|
||||||
this.updatePlayer(infoHash, index, true, cb)
|
this.updatePlayer(infoHash, index, true, cb)
|
||||||
@@ -236,12 +237,15 @@ export default class PlaybackController {
|
|||||||
// Starts WebTorrent server for media streaming
|
// Starts WebTorrent server for media streaming
|
||||||
startServer (torrentSummary) {
|
startServer (torrentSummary) {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
|
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
||||||
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, () => onTorrentReady())
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||||
|
() => onTorrentReady())
|
||||||
} else {
|
} else {
|
||||||
onTorrentReady()
|
onTorrentReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTorrentReady () {
|
function onTorrentReady () {
|
||||||
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
|
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
|
||||||
ipcRenderer.once('wt-server-running', () => { state.playing.isReady = true })
|
ipcRenderer.once('wt-server-running', () => { state.playing.isReady = true })
|
||||||
@@ -251,23 +255,26 @@ export default class PlaybackController {
|
|||||||
// Called each time the current file changes
|
// Called each time the current file changes
|
||||||
updatePlayer (infoHash, index, resume, cb) {
|
updatePlayer (infoHash, index, resume, cb) {
|
||||||
const state = this.state
|
const state = this.state
|
||||||
|
|
||||||
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||||
const fileSummary = torrentSummary.files[index]
|
const fileSummary = torrentSummary.files[index]
|
||||||
|
|
||||||
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
||||||
torrentSummary.mostRecentFileIndex = undefined
|
torrentSummary.mostRecentFileIndex = undefined
|
||||||
return cb(new UnplayableFileError())
|
return cb(new UnplayableFileError())
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentSummary.mostRecentFileIndex = index
|
torrentSummary.mostRecentFileIndex = index
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
state.playing.infoHash = infoHash
|
state.playing.infoHash = infoHash
|
||||||
state.playing.fileIndex = index
|
state.playing.fileIndex = index
|
||||||
state.playing.fileName = fileSummary.name
|
state.playing.fileName = fileSummary.name
|
||||||
state.playing.type = TorrentPlayer.isVideo(fileSummary)
|
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
||||||
? 'video'
|
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||||
: TorrentPlayer.isAudio(fileSummary)
|
|
||||||
? 'audio'
|
|
||||||
: 'other'
|
: 'other'
|
||||||
// pick up where we left off
|
|
||||||
|
// pick up where we left off
|
||||||
let jumpToTime = 0
|
let jumpToTime = 0
|
||||||
if (resume && fileSummary.currentTime) {
|
if (resume && fileSummary.currentTime) {
|
||||||
const fraction = fileSummary.currentTime / fileSummary.duration
|
const fraction = fileSummary.currentTime / fileSummary.duration
|
||||||
@@ -277,24 +284,30 @@ export default class PlaybackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.playing.jumpToTime = jumpToTime
|
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 (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata)
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata)
|
||||||
} else {
|
} else {
|
||||||
getAudioMetadata()
|
getAudioMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAudioMetadata () {
|
function getAudioMetadata () {
|
||||||
if (state.playing.type === 'audio') {
|
if (state.playing.type === 'audio') {
|
||||||
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's video, check for subtitles files that are done downloading
|
// if it's video, check for subtitles files that are done downloading
|
||||||
dispatch('checkForSubtitles')
|
dispatch('checkForSubtitles')
|
||||||
|
|
||||||
// enable previously selected subtitle track
|
// enable previously selected subtitle track
|
||||||
if (fileSummary.selectedSubtitle) {
|
if (fileSummary.selectedSubtitle) {
|
||||||
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.window.title = fileSummary.name
|
state.window.title = fileSummary.name
|
||||||
|
|
||||||
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
||||||
if (this.state.saved.prefs.openExternalPlayer) {
|
if (this.state.saved.prefs.openExternalPlayer) {
|
||||||
dispatch('openExternalPlayer')
|
dispatch('openExternalPlayer')
|
||||||
@@ -302,8 +315,10 @@ export default class PlaybackController {
|
|||||||
cb()
|
cb()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, play the video
|
// otherwise, play the video
|
||||||
this.update()
|
this.update()
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
|
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
@@ -334,13 +349,33 @@ export default class PlaybackController {
|
|||||||
dispatch('toggleFullScreen', false)
|
dispatch('toggleFullScreen', false)
|
||||||
}
|
}
|
||||||
restoreBounds(state)
|
restoreBounds(state)
|
||||||
|
|
||||||
// Tell the WebTorrent process to kill the torrent-to-HTTP server
|
// Tell the WebTorrent process to kill the torrent-to-HTTP server
|
||||||
ipcRenderer.send('wt-stop-server')
|
ipcRenderer.send('wt-stop-server')
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerClose')
|
ipcRenderer.send('onPlayerClose')
|
||||||
|
|
||||||
// Playback Priority: resume previously paused downloads.
|
// Playback Priority: resume previously paused downloads.
|
||||||
if (this.state.saved.prefs.highestPlaybackPriority) {
|
if (this.state.saved.prefs.highestPlaybackPriority) {
|
||||||
dispatch('resumePausedTorrents')
|
dispatch('resumePausedTorrents')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update()
|
this.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks whether we are connected and already casting
|
||||||
|
// Returns false if we not casting (state.playing.location === 'local')
|
||||||
|
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
|
||||||
|
function isCasting (state) {
|
||||||
|
return state.playing.location === 'chromecast' ||
|
||||||
|
state.playing.location === 'airplay' ||
|
||||||
|
state.playing.location === 'dlna'
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreBounds (state) {
|
||||||
|
ipcRenderer.send('setAspectRatio', 0)
|
||||||
|
if (state.window.bounds) {
|
||||||
|
ipcRenderer.send('setBounds', state.window.bounds, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { dispatch } from '../lib/dispatcher.js'
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
import { ipcRenderer } from 'electron'
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
export default class PrefsController {
|
// Controls the Preferences screen
|
||||||
|
module.exports = class PrefsController {
|
||||||
constructor (state, config) {
|
constructor (state, config) {
|
||||||
this.state = state
|
this.state = state
|
||||||
this.config = config
|
this.config = config
|
||||||
@@ -12,7 +13,7 @@ export default class PrefsController {
|
|||||||
const state = this.state
|
const state = this.state
|
||||||
state.location.go({
|
state.location.go({
|
||||||
url: 'preferences',
|
url: 'preferences',
|
||||||
setup (cb) {
|
setup: function (cb) {
|
||||||
// initialize preferences
|
// initialize preferences
|
||||||
state.window.title = 'Preferences'
|
state.window.title = 'Preferences'
|
||||||
ipcRenderer.send('setAllowNav', false)
|
ipcRenderer.send('setAllowNav', false)
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import remote from '@electron/remote'
|
const electron = require('electron')
|
||||||
import fs from 'fs'
|
const fs = require('fs')
|
||||||
import path from 'path'
|
const path = require('path')
|
||||||
import parallel from 'run-parallel'
|
const parallel = require('run-parallel')
|
||||||
import { dispatch } from '../lib/dispatcher.js'
|
|
||||||
|
|
||||||
export default class SubtitlesController {
|
const remote = electron.remote
|
||||||
|
|
||||||
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class SubtitlesController {
|
||||||
constructor (state) {
|
constructor (state) {
|
||||||
this.state = state
|
this.state = state
|
||||||
}
|
}
|
||||||
|
|
||||||
openSubtitles () {
|
openSubtitles () {
|
||||||
const filenames = remote.dialog.showOpenDialogSync({
|
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']
|
||||||
|
}, (filenames) => {
|
||||||
|
if (!Array.isArray(filenames)) return
|
||||||
|
this.addSubtitles(filenames, true)
|
||||||
})
|
})
|
||||||
if (!Array.isArray(filenames)) return
|
|
||||||
this.addSubtitles(filenames, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectSubtitle (ix) {
|
selectSubtitle (ix) {
|
||||||
@@ -36,11 +40,12 @@ export default class SubtitlesController {
|
|||||||
|
|
||||||
// Read the files concurrently, then add all resulting subtitle tracks
|
// Read the files concurrently, then add all resulting subtitle tracks
|
||||||
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||||
parallel(tasks, (err, tracks) => {
|
parallel(tasks, function (err, tracks) {
|
||||||
if (err) return dispatch('error', err)
|
if (err) return dispatch('error', err)
|
||||||
|
|
||||||
// No dupes allowed
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
tracks.forEach((track, i) => {
|
// No dupes allowed
|
||||||
|
const track = tracks[i]
|
||||||
let trackIndex = subtitles.tracks.findIndex((t) =>
|
let trackIndex = subtitles.tracks.findIndex((t) =>
|
||||||
track.filePath === t.filePath)
|
track.filePath === t.filePath)
|
||||||
|
|
||||||
@@ -53,7 +58,7 @@ export default class SubtitlesController {
|
|||||||
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||||
subtitles.selectedIndex = trackIndex
|
subtitles.selectedIndex = trackIndex
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
// Finally, make sure no two tracks have the same label
|
// Finally, make sure no two tracks have the same label
|
||||||
relabelSubtitles(subtitles)
|
relabelSubtitles(subtitles)
|
||||||
@@ -81,18 +86,18 @@ export default class SubtitlesController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSubtitle (file, cb) {
|
function loadSubtitle (file, cb) {
|
||||||
// Lazy load to keep startup fast
|
// Lazy load to keep startup fast
|
||||||
const concat = await import('simple-concat')
|
const concat = require('simple-concat')
|
||||||
const LanguageDetect = await import('languagedetect')
|
const LanguageDetect = require('languagedetect')
|
||||||
const srtToVtt = await import('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
|
||||||
const filePath = file.path || file
|
const filePath = file.path || file
|
||||||
|
|
||||||
const vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
const vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
||||||
|
|
||||||
concat(vttStream, (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
|
||||||
@@ -105,7 +110,7 @@ async function loadSubtitle (file, cb) {
|
|||||||
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
||||||
language: langDetected,
|
language: langDetected,
|
||||||
label: langDetected,
|
label: langDetected,
|
||||||
filePath
|
filePath: filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(null, track)
|
cb(null, track)
|
||||||
@@ -114,8 +119,8 @@ async 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
|
||||||
async function isSystemLanguage (language) {
|
function isSystemLanguage (language) {
|
||||||
const iso639 = await import('iso-639-1')
|
const iso639 = require('iso-639-1')
|
||||||
const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
|
const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
|
||||||
const 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
|
||||||
@@ -125,7 +130,7 @@ async function isSystemLanguage (language) {
|
|||||||
// 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) {
|
||||||
const counts = {}
|
const counts = {}
|
||||||
subtitles.tracks.forEach(track => {
|
subtitles.tracks.forEach(function (track) {
|
||||||
const 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
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import path from 'path'
|
const path = require('path')
|
||||||
import { ipcRenderer } from 'electron'
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
import TorrentSummary from '../lib/torrent-summary.js'
|
|
||||||
import sound from '../lib/sound.js'
|
|
||||||
import { dispatch } from '../lib/dispatcher.js'
|
|
||||||
|
|
||||||
export default class TorrentController {
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const sound = require('../lib/sound')
|
||||||
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class TorrentController {
|
||||||
constructor (state) {
|
constructor (state) {
|
||||||
this.state = state
|
this.state = state
|
||||||
}
|
}
|
||||||
@@ -24,7 +25,7 @@ export default class TorrentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
torrentSummary = {
|
torrentSummary = {
|
||||||
torrentKey,
|
torrentKey: torrentKey,
|
||||||
status: 'new'
|
status: 'new'
|
||||||
}
|
}
|
||||||
torrents.unshift(torrentSummary)
|
torrents.unshift(torrentSummary)
|
||||||
@@ -176,7 +177,7 @@ function showDoneNotification (torrent) {
|
|||||||
silent: true
|
silent: true
|
||||||
})
|
})
|
||||||
|
|
||||||
notif.onclick = () => {
|
notif.onclick = function () {
|
||||||
ipcRenderer.send('show')
|
ipcRenderer.send('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import fs from 'fs'
|
const fs = require('fs')
|
||||||
import path from 'path'
|
const path = require('path')
|
||||||
import electron from 'electron'
|
const electron = require('electron')
|
||||||
import remote from '@electron/remote'
|
|
||||||
|
|
||||||
import { dispatch } from '../lib/dispatcher.js'
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
import { TorrentKeyNotFoundError } from '../lib/errors.js'
|
const { TorrentKeyNotFoundError } = require('../lib/errors')
|
||||||
import sound from '../lib/sound.js'
|
const sound = require('../lib/sound')
|
||||||
import TorrentSummary from '../lib/torrent-summary.js'
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
|
||||||
const { ipcRenderer, clipboard } = electron
|
const ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
const instantIoRegex = /^(https:\/\/)?instant\.io\/#/
|
const instantIoRegex = /^(https:\/\/)?instant\.io\/#/
|
||||||
|
|
||||||
export default class TorrentListController {
|
// Controls the torrent list: creating, adding, deleting, & manipulating torrents
|
||||||
|
module.exports = class TorrentListController {
|
||||||
constructor (state) {
|
constructor (state) {
|
||||||
this.state = state
|
this.state = state
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export default class TorrentListController {
|
|||||||
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,
|
||||||
setup: (cb) => {
|
setup: (cb) => {
|
||||||
this.state.window.title = 'Create New Torrent'
|
this.state.window.title = 'Create New Torrent'
|
||||||
cb(null)
|
cb(null)
|
||||||
@@ -95,7 +95,7 @@ export default class TorrentListController {
|
|||||||
if (!fileOrFolder) return start()
|
if (!fileOrFolder) return start()
|
||||||
|
|
||||||
// Existing torrent: check that the path is still there
|
// Existing torrent: check that the path is still there
|
||||||
fs.stat(fileOrFolder, err => {
|
fs.stat(fileOrFolder, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
s.error = 'path-missing'
|
s.error = 'path-missing'
|
||||||
dispatch('backToList')
|
dispatch('backToList')
|
||||||
@@ -157,8 +157,10 @@ export default class TorrentListController {
|
|||||||
|
|
||||||
prioritizeTorrent (infoHash) {
|
prioritizeTorrent (infoHash) {
|
||||||
this.state.saved.torrents
|
this.state.saved.torrents
|
||||||
.filter(torrent => ['downloading', 'seeding'].includes(torrent.status)) // Active torrents only.
|
.filter((torrent) => { // We're interested in active torrents only.
|
||||||
.forEach((torrent) => { // Pause all active torrents except the one that started playing.
|
return (['downloading', 'seeding'].indexOf(torrent.status) !== -1)
|
||||||
|
})
|
||||||
|
.map((torrent) => { // Pause all active torrents except the one that started playing.
|
||||||
if (infoHash === torrent.infoHash) return
|
if (infoHash === torrent.infoHash) return
|
||||||
|
|
||||||
// Pause torrent without playing sounds.
|
// Pause torrent without playing sounds.
|
||||||
@@ -173,7 +175,7 @@ export default class TorrentListController {
|
|||||||
resumePausedTorrents () {
|
resumePausedTorrents () {
|
||||||
console.log('Playback Priority: resuming paused torrents')
|
console.log('Playback Priority: resuming paused torrents')
|
||||||
if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return
|
if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return
|
||||||
this.state.saved.torrentsToResume.forEach((infoHash) => {
|
this.state.saved.torrentsToResume.map((infoHash) => {
|
||||||
this.toggleTorrent(infoHash)
|
this.toggleTorrent(infoHash)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -199,43 +201,26 @@ export default class TorrentListController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmDeleteAllTorrents (deleteData) {
|
|
||||||
this.state.modal = {
|
|
||||||
id: 'delete-all-torrents-modal',
|
|
||||||
deleteData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
deleteTorrent (infoHash, deleteData) {
|
deleteTorrent (infoHash, deleteData) {
|
||||||
|
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||||
|
|
||||||
const 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) {
|
||||||
const summary = this.state.saved.torrents[index]
|
const summary = this.state.saved.torrents[index]
|
||||||
deleteTorrentFile(summary, deleteData)
|
|
||||||
|
// remove torrent and poster file
|
||||||
|
deleteFile(TorrentSummary.getTorrentPath(summary))
|
||||||
|
deleteFile(TorrentSummary.getPosterPath(summary))
|
||||||
|
|
||||||
|
// optionally delete the torrent data
|
||||||
|
if (deleteData) moveItemToTrash(summary)
|
||||||
|
|
||||||
// remove torrent from saved list
|
// remove torrent from saved list
|
||||||
this.state.saved.torrents.splice(index, 1)
|
this.state.saved.torrents.splice(index, 1)
|
||||||
dispatch('stateSave')
|
dispatch('stateSave')
|
||||||
|
|
||||||
// prevent user from going forward to a deleted torrent
|
|
||||||
this.state.location.clearForward('player')
|
|
||||||
sound.play('DELETE')
|
|
||||||
} else {
|
|
||||||
throw new TorrentKeyNotFoundError(infoHash)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
deleteAllTorrents (deleteData) {
|
|
||||||
// Go back to list before the current playing torrent is deleted
|
|
||||||
if (this.state.location.url() === 'player') {
|
|
||||||
dispatch('backToList')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.saved.torrents.forEach((summary) => deleteTorrentFile(summary, deleteData))
|
|
||||||
|
|
||||||
this.state.saved.torrents = []
|
|
||||||
dispatch('stateSave')
|
|
||||||
|
|
||||||
// prevent user from going forward to a deleted torrent
|
// prevent user from going forward to a deleted torrent
|
||||||
this.state.location.clearForward('player')
|
this.state.location.clearForward('player')
|
||||||
@@ -252,88 +237,77 @@ export default class TorrentListController {
|
|||||||
|
|
||||||
openTorrentContextMenu (infoHash) {
|
openTorrentContextMenu (infoHash) {
|
||||||
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
const menu = new remote.Menu()
|
const menu = new electron.remote.Menu()
|
||||||
|
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Remove From List',
|
label: 'Remove From List',
|
||||||
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
|
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Remove Data File',
|
label: 'Remove Data File',
|
||||||
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
|
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (torrentSummary.files) {
|
if (torrentSummary.files) {
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
|
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
|
||||||
click: () => showItemInFolder(torrentSummary)
|
click: () => showItemInFolder(torrentSummary)
|
||||||
}))
|
}))
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Copy Magnet Link to Clipboard',
|
label: 'Copy Magnet Link to Clipboard',
|
||||||
click: () => clipboard.writeText(torrentSummary.magnetURI)
|
click: () => electron.clipboard.writeText(torrentSummary.magnetURI)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Copy Instant.io Link to Clipboard',
|
label: 'Copy Instant.io Link to Clipboard',
|
||||||
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
|
click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Save Torrent File As...',
|
label: 'Save Torrent File As...',
|
||||||
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey),
|
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey),
|
||||||
enabled: torrentSummary.torrentFileName != null
|
enabled: torrentSummary.torrentFileName != null
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new remote.MenuItem({
|
menu.popup(electron.remote.getCurrentWindow())
|
||||||
type: 'separator'
|
|
||||||
}))
|
|
||||||
|
|
||||||
const sortedByName = this.state.saved.prefs.sortByName
|
|
||||||
menu.append(new remote.MenuItem({
|
|
||||||
label: `${sortedByName ? '✓ ' : ''}Sort by Name`,
|
|
||||||
click: () => dispatch('updatePreferences', 'sortByName', !sortedByName)
|
|
||||||
}))
|
|
||||||
|
|
||||||
menu.popup({ window: remote.getCurrentWindow() })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Takes a torrentSummary or torrentKey
|
// Takes a torrentSummary or torrentKey
|
||||||
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
|
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
|
||||||
saveTorrentFileAs (torrentKey) {
|
saveTorrentFileAs (torrentKey) {
|
||||||
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
|
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
if (!torrentSummary) throw new TorrentKeyNotFoundError(torrentKey)
|
if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
|
||||||
const downloadPath = this.state.saved.prefs.downloadPath
|
const downloadPath = this.state.saved.prefs.downloadPath
|
||||||
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||||
const win = remote.getCurrentWindow()
|
const win = electron.remote.getCurrentWindow()
|
||||||
const opts = {
|
const opts = {
|
||||||
title: 'Save Torrent File',
|
title: 'Save Torrent File',
|
||||||
defaultPath: path.join(downloadPath, newFileName),
|
defaultPath: path.join(downloadPath, newFileName),
|
||||||
filters: [
|
filters: [
|
||||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||||
{ name: 'All Files', extensions: ['*'] }
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
],
|
]
|
||||||
buttonLabel: 'Save'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const savePath = remote.dialog.showSaveDialogSync(win, opts)
|
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
|
||||||
|
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
|
||||||
if (!savePath) return // They clicked Cancel
|
if (!savePath) return // They clicked Cancel
|
||||||
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
|
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||||
fs.readFile(torrentPath, (err, torrentFile) => {
|
|
||||||
if (err) return dispatch('error', err)
|
|
||||||
fs.writeFile(savePath, torrentFile, err => {
|
|
||||||
if (err) return dispatch('error', err)
|
if (err) return dispatch('error', err)
|
||||||
|
fs.writeFile(savePath, torrentFile, function (err) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -345,11 +319,11 @@ function findFilesRecursive (paths, cb_) {
|
|||||||
if (paths.length > 1) {
|
if (paths.length > 1) {
|
||||||
let numComplete = 0
|
let numComplete = 0
|
||||||
const ret = []
|
const ret = []
|
||||||
paths.forEach(path => {
|
paths.forEach(function (path) {
|
||||||
findFilesRecursive([path], fileObjs => {
|
findFilesRecursive([path], function (fileObjs) {
|
||||||
ret.push(...fileObjs)
|
ret.push(...fileObjs)
|
||||||
if (++numComplete === paths.length) {
|
if (++numComplete === paths.length) {
|
||||||
ret.sort((a, b) => a.path < b.path ? -1 : Number(a.path > b.path))
|
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
|
||||||
cb_(ret)
|
cb_(ret)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -358,7 +332,7 @@ function findFilesRecursive (paths, cb_) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileOrFolder = paths[0]
|
const fileOrFolder = paths[0]
|
||||||
fs.stat(fileOrFolder, (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
|
||||||
@@ -373,7 +347,7 @@ function findFilesRecursive (paths, cb_) {
|
|||||||
|
|
||||||
// Folders: recurse, make a list of all the files
|
// Folders: recurse, make a list of all the files
|
||||||
const folderPath = fileOrFolder
|
const folderPath = fileOrFolder
|
||||||
fs.readdir(folderPath, (err, fileNames) => {
|
fs.readdir(folderPath, function (err, fileNames) {
|
||||||
if (err) return dispatch('error', err)
|
if (err) return dispatch('error', err)
|
||||||
const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
||||||
findFilesRecursive(paths, cb_)
|
findFilesRecursive(paths, cb_)
|
||||||
@@ -383,7 +357,7 @@ function findFilesRecursive (paths, cb_) {
|
|||||||
|
|
||||||
function deleteFile (path) {
|
function deleteFile (path) {
|
||||||
if (!path) return
|
if (!path) return
|
||||||
fs.unlink(path, err => {
|
fs.unlink(path, function (err) {
|
||||||
if (err) dispatch('error', err)
|
if (err) dispatch('error', err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -397,14 +371,3 @@ function moveItemToTrash (torrentSummary) {
|
|||||||
function showItemInFolder (torrentSummary) {
|
function showItemInFolder (torrentSummary) {
|
||||||
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteTorrentFile (torrentSummary, deleteData) {
|
|
||||||
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
|
||||||
|
|
||||||
// remove torrent and poster file
|
|
||||||
deleteFile(TorrentSummary.getTorrentPath(torrentSummary))
|
|
||||||
deleteFile(TorrentSummary.getPosterPath(torrentSummary))
|
|
||||||
|
|
||||||
// optionally delete the torrent data
|
|
||||||
if (deleteData) moveItemToTrash(torrentSummary)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { dispatch } from '../lib/dispatcher.js'
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
|
|
||||||
export default class UpdateController {
|
// Controls the UI checking for new versions of the app, prompting install
|
||||||
|
module.exports = class UpdateController {
|
||||||
constructor (state) {
|
constructor (state) {
|
||||||
this.state = state
|
this.state = state
|
||||||
}
|
}
|
||||||
@@ -12,7 +13,7 @@ export default class UpdateController {
|
|||||||
console.log('new version skipped by user: v' + version)
|
console.log('new version skipped by user: v' + version)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.state.modal = { id: 'update-available-modal', version }
|
this.state.modal = { id: 'update-available-modal', version: version }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't show the modal again until the next version
|
// Don't show the modal again until the next version
|
||||||
|
|||||||
@@ -1,18 +1,40 @@
|
|||||||
import http from 'http'
|
// The Cast module talks to Airplay and Chromecast
|
||||||
import config from '../src/config.js'
|
// * Modifies state when things change
|
||||||
import { CastingError } from './errors.js'
|
// * Starts and stops casting, provides remote video controls
|
||||||
|
module.exports = {
|
||||||
|
init,
|
||||||
|
toggleMenu,
|
||||||
|
selectDevice,
|
||||||
|
stop,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
seek,
|
||||||
|
setVolume,
|
||||||
|
setRate
|
||||||
|
}
|
||||||
|
|
||||||
|
const http = require('http')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
const { CastingError } = require('./errors')
|
||||||
|
|
||||||
// Lazy load these for a ~300ms improvement in startup time
|
// Lazy load these for a ~300ms improvement in startup time
|
||||||
let airplayer, chromecasts, dlnacasts
|
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
|
||||||
let state
|
let state
|
||||||
|
|
||||||
// Callback to notify module users when state has changed
|
// Callback to notify module users when state has changed
|
||||||
let update
|
let update
|
||||||
|
|
||||||
// setInterval() for updating cast status
|
// setInterval() for updating cast status
|
||||||
let 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) {
|
||||||
state = appState
|
state = appState
|
||||||
update = callback
|
update = callback
|
||||||
|
|
||||||
// Don't actually cast during integration tests
|
// Don't actually cast during integration tests
|
||||||
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
|
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
|
||||||
if (config.IS_TEST) {
|
if (config.IS_TEST) {
|
||||||
@@ -21,26 +43,32 @@ function init (appState, callback) {
|
|||||||
state.devices.dlna = testPlayer('dlna')
|
state.devices.dlna = testPlayer('dlna')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load modules, scan the network for devices
|
// Load modules, scan the network for devices
|
||||||
airplayer = require('airplayer')()
|
airplayer = require('airplayer')()
|
||||||
chromecasts = require('chromecasts')()
|
chromecasts = require('chromecasts')()
|
||||||
dlnacasts = require('dlnacasts')()
|
dlnacasts = require('dlnacasts')()
|
||||||
|
|
||||||
state.devices.chromecast = chromecastPlayer()
|
state.devices.chromecast = chromecastPlayer()
|
||||||
state.devices.dlna = dlnaPlayer()
|
state.devices.dlna = dlnaPlayer()
|
||||||
state.devices.airplay = airplayPlayer()
|
state.devices.airplay = airplayPlayer()
|
||||||
|
|
||||||
// Listen for devices: Chromecast, DLNA and Airplay
|
// Listen for devices: Chromecast, DLNA and Airplay
|
||||||
chromecasts.on('update', device => {
|
chromecasts.on('update', function (device) {
|
||||||
// TODO: how do we tell if there are *no longer* any Chromecasts available?
|
// TODO: how do we tell if there are *no longer* any Chromecasts available?
|
||||||
// From looking at the code, chromecasts.players only grows, never shrinks
|
// From looking at the code, chromecasts.players only grows, never shrinks
|
||||||
state.devices.chromecast.addDevice(device)
|
state.devices.chromecast.addDevice(device)
|
||||||
})
|
})
|
||||||
dlnacasts.on('update', device => {
|
|
||||||
|
dlnacasts.on('update', function (device) {
|
||||||
state.devices.dlna.addDevice(device)
|
state.devices.dlna.addDevice(device)
|
||||||
})
|
})
|
||||||
airplayer.on('update', device => {
|
|
||||||
|
airplayer.on('update', function (device) {
|
||||||
state.devices.airplay.addDevice(device)
|
state.devices.airplay.addDevice(device)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// integration test player implementation
|
// integration test player implementation
|
||||||
function testPlayer (type) {
|
function testPlayer (type) {
|
||||||
return {
|
return {
|
||||||
@@ -53,17 +81,20 @@ function testPlayer (type) {
|
|||||||
seek,
|
seek,
|
||||||
volume
|
volume
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDevices () {
|
function getDevices () {
|
||||||
return [{ name: type + '-1' }, { name: type + '-2' }]
|
return [{ name: type + '-1' }, { name: type + '-2' }]
|
||||||
}
|
}
|
||||||
function open () { }
|
|
||||||
function play () { }
|
function open () {}
|
||||||
function pause () { }
|
function play () {}
|
||||||
function stop () { }
|
function pause () {}
|
||||||
function status () { }
|
function stop () {}
|
||||||
function seek () { }
|
function status () {}
|
||||||
function volume () { }
|
function seek () {}
|
||||||
|
function volume () {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// chromecast player implementation
|
// chromecast player implementation
|
||||||
function chromecastPlayer () {
|
function chromecastPlayer () {
|
||||||
const ret = {
|
const ret = {
|
||||||
@@ -79,11 +110,13 @@ function chromecastPlayer () {
|
|||||||
volume
|
volume
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
function getDevices () {
|
function getDevices () {
|
||||||
return chromecasts.players
|
return chromecasts.players
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDevice (device) {
|
function addDevice (device) {
|
||||||
device.on('error', err => {
|
device.on('error', function (err) {
|
||||||
if (device !== ret.device) return
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -92,12 +125,13 @@ function chromecastPlayer () {
|
|||||||
})
|
})
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
device.on('disconnect', () => {
|
device.on('disconnect', function () {
|
||||||
if (device !== ret.device) return
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function serveSubtitles (callback) {
|
function serveSubtitles (callback) {
|
||||||
const subtitles = state.playing.subtitles
|
const subtitles = state.playing.subtitles
|
||||||
const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex]
|
const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex]
|
||||||
@@ -111,22 +145,23 @@ function chromecastPlayer () {
|
|||||||
'Transfer-Encoding': 'chunked'
|
'Transfer-Encoding': 'chunked'
|
||||||
})
|
})
|
||||||
res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64'))
|
res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64'))
|
||||||
}).listen(0, () => {
|
}).listen(0, function () {
|
||||||
const port = ret.subServer.address().port
|
const port = ret.subServer.address().port
|
||||||
const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/'
|
const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/'
|
||||||
callback(subtitlesUrl)
|
callback(subtitlesUrl)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
serveSubtitles(subtitlesUrl => {
|
serveSubtitles(function (subtitlesUrl) {
|
||||||
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
|
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,
|
||||||
subtitles: subtitlesUrl ? [subtitlesUrl] : [],
|
subtitles: subtitlesUrl ? [subtitlesUrl] : [],
|
||||||
autoSubtitles: !!subtitlesUrl
|
autoSubtitles: !!subtitlesUrl
|
||||||
}, err => {
|
}, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -140,28 +175,35 @@ function chromecastPlayer () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
ret.device.play(null, null, callback)
|
ret.device.play(null, null, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
ret.device.pause(callback)
|
ret.device.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
ret.device.stop(callback)
|
ret.device.stop(callback)
|
||||||
if (ret.subServer) {
|
if (ret.subServer) {
|
||||||
ret.subServer.close()
|
ret.subServer.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
ret.device.status(handleStatus)
|
ret.device.status(handleStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
ret.device.seek(time, callback)
|
ret.device.seek(time, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, callback) {
|
function volume (volume, callback) {
|
||||||
ret.device.volume(volume, callback)
|
ret.device.volume(volume, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// airplay player implementation
|
// airplay player implementation
|
||||||
function airplayPlayer () {
|
function airplayPlayer () {
|
||||||
const ret = {
|
const ret = {
|
||||||
@@ -177,8 +219,9 @@ function airplayPlayer () {
|
|||||||
volume
|
volume
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
function addDevice (player) {
|
function addDevice (player) {
|
||||||
player.on('event', event => {
|
player.on('event', function (event) {
|
||||||
switch (event.state) {
|
switch (event.state) {
|
||||||
case 'loading':
|
case 'loading':
|
||||||
break
|
break
|
||||||
@@ -194,11 +237,13 @@ function airplayPlayer () {
|
|||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDevices () {
|
function getDevices () {
|
||||||
return airplayer.players
|
return airplayer.players
|
||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, (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({
|
||||||
@@ -211,17 +256,21 @@ function airplayPlayer () {
|
|||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
ret.device.resume(callback)
|
ret.device.resume(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
ret.device.pause(callback)
|
ret.device.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
ret.device.stop(callback)
|
ret.device.stop(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
ret.device.playbackInfo((err, res, status) => {
|
ret.device.playbackInfo(function (err, res, status) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -235,15 +284,18 @@ function airplayPlayer () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
ret.device.scrub(time, callback)
|
ret.device.scrub(time, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, callback) {
|
function volume (volume, callback) {
|
||||||
// AirPlay doesn't support volume
|
// AirPlay doesn't support volume
|
||||||
// TODO: We should just disable the volume slider
|
// TODO: We should just disable the volume slider
|
||||||
state.playing.volume = volume
|
state.playing.volume = volume
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DLNA player implementation
|
// DLNA player implementation
|
||||||
function dlnaPlayer (player) {
|
function dlnaPlayer (player) {
|
||||||
const ret = {
|
const ret = {
|
||||||
@@ -259,11 +311,13 @@ function dlnaPlayer (player) {
|
|||||||
volume
|
volume
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
function getDevices () {
|
function getDevices () {
|
||||||
return dlnacasts.players
|
return dlnacasts.players
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDevice (device) {
|
function addDevice (device) {
|
||||||
device.on('error', err => {
|
device.on('error', function (err) {
|
||||||
if (device !== ret.device) return
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -272,19 +326,20 @@ function dlnaPlayer (player) {
|
|||||||
})
|
})
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
device.on('disconnect', () => {
|
device.on('disconnect', function () {
|
||||||
if (device !== ret.device) return
|
if (device !== ret.device) return
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
const 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 + '/' + state.playing.fileIndex, {
|
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
|
||||||
}, err => {
|
}, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -297,45 +352,56 @@ function dlnaPlayer (player) {
|
|||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function play (callback) {
|
function play (callback) {
|
||||||
ret.device.play(null, null, callback)
|
ret.device.play(null, null, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause (callback) {
|
function pause (callback) {
|
||||||
ret.device.pause(callback)
|
ret.device.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
ret.device.stop(callback)
|
ret.device.stop(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
ret.device.status(handleStatus)
|
ret.device.status(handleStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
ret.device.seek(time, callback)
|
ret.device.seek(time, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function volume (volume, callback) {
|
function volume (volume, callback) {
|
||||||
ret.device.volume(volume, err => {
|
ret.device.volume(volume, function (err) {
|
||||||
// quick volume update
|
// quick volume update
|
||||||
state.playing.volume = volume
|
state.playing.volume = volume
|
||||||
callback(err)
|
callback(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStatus (err, status) {
|
function handleStatus (err, status) {
|
||||||
if (err || !status) {
|
if (err || !status) {
|
||||||
return console.log('error getting %s status: %o', state.playing.location, err || 'missing response')
|
return console.log('error getting %s status: %o',
|
||||||
|
state.playing.location,
|
||||||
|
err || 'missing response')
|
||||||
}
|
}
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||||
state.playing.currentTime = status.currentTime
|
state.playing.currentTime = status.currentTime
|
||||||
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start polling cast device state, whenever we're connected
|
// Start polling cast device state, whenever we're connected
|
||||||
function startStatusInterval () {
|
function startStatusInterval () {
|
||||||
statusInterval = setInterval(() => {
|
statusInterval = setInterval(function () {
|
||||||
const player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.status()
|
if (player) player.status()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
|
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
|
||||||
* The menu lists eg. all Chromecasts detected; the user can click one to cast.
|
* The menu lists eg. all Chromecasts detected; the user can click one to cast.
|
||||||
@@ -347,38 +413,48 @@ function toggleMenu (location) {
|
|||||||
state.devices.castMenu = null
|
state.devices.castMenu = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 CastingError(`You can't connect to ${location} when already connected to another device`)
|
throw new CastingError(
|
||||||
|
`You can't connect to ${location} when already connected to another device`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all cast devices of the given type
|
// Find all cast devices of the given type
|
||||||
const player = getPlayer(location)
|
const player = getPlayer(location)
|
||||||
const devices = player ? player.getDevices() : []
|
const devices = player ? player.getDevices() : []
|
||||||
if (devices.length === 0) {
|
if (devices.length === 0) {
|
||||||
throw new CastingError(`No ${location} devices available`)
|
throw new CastingError(`No ${location} devices available`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a menu
|
// Show a menu
|
||||||
state.devices.castMenu = { location, devices }
|
state.devices.castMenu = { location, devices }
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectDevice (index) {
|
function selectDevice (index) {
|
||||||
const { location, devices } = state.devices.castMenu
|
const { location, devices } = state.devices.castMenu
|
||||||
|
|
||||||
// Start casting
|
// Start casting
|
||||||
const player = getPlayer(location)
|
const player = getPlayer(location)
|
||||||
player.device = devices[index]
|
player.device = devices[index]
|
||||||
player.open()
|
player.open()
|
||||||
|
|
||||||
// Poll the casting device's status every few seconds
|
// Poll the casting device's status every few seconds
|
||||||
startStatusInterval()
|
startStatusInterval()
|
||||||
|
|
||||||
// Show the Connecting... screen
|
// Show the Connecting... screen
|
||||||
state.devices.castMenu = null
|
state.devices.castMenu = null
|
||||||
state.playing.castName = devices[index].name
|
state.playing.castName = devices[index].name
|
||||||
state.playing.location = location + '-pending'
|
state.playing.location = location + '-pending'
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stops casting, move video back to local screen
|
// Stops casting, move video back to local screen
|
||||||
function stop () {
|
function stop () {
|
||||||
const player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) {
|
if (player) {
|
||||||
player.stop(() => {
|
player.stop(function () {
|
||||||
player.device = null
|
player.device = null
|
||||||
stoppedCasting()
|
stoppedCasting()
|
||||||
})
|
})
|
||||||
@@ -387,6 +463,7 @@ function stop () {
|
|||||||
stoppedCasting()
|
stoppedCasting()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stoppedCasting () {
|
function stoppedCasting () {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.playing.jumpToTime = Number.isFinite(state.playing.currentTime)
|
state.playing.jumpToTime = Number.isFinite(state.playing.currentTime)
|
||||||
@@ -394,6 +471,7 @@ function stoppedCasting () {
|
|||||||
: 0
|
: 0
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayer (location) {
|
function getPlayer (location) {
|
||||||
if (location) {
|
if (location) {
|
||||||
return state.devices[location]
|
return state.devices[location]
|
||||||
@@ -407,14 +485,17 @@ function getPlayer (location) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function play () {
|
function play () {
|
||||||
const player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.play(castCallback)
|
if (player) player.play(castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause () {
|
function pause () {
|
||||||
const player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.pause(castCallback)
|
if (player) player.pause(castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRate (rate) {
|
function setRate (rate) {
|
||||||
let player
|
let player
|
||||||
let result = true
|
let result = true
|
||||||
@@ -430,34 +511,17 @@ function setRate (rate) {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time) {
|
function seek (time) {
|
||||||
const player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) { player.seek(time, castCallback) }
|
if (player) player.seek(time, castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVolume (volume) {
|
function setVolume (volume) {
|
||||||
const player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) { player.volume(volume, castCallback) }
|
if (player) player.volume(volume, castCallback)
|
||||||
}
|
}
|
||||||
function castCallback (...args) {
|
|
||||||
console.log('%s callback: %o', state.playing.location, args)
|
function castCallback () {
|
||||||
}
|
console.log('%s callback: %o', state.playing.location, arguments)
|
||||||
export { init }
|
|
||||||
export { toggleMenu }
|
|
||||||
export { selectDevice }
|
|
||||||
export { stop }
|
|
||||||
export { play }
|
|
||||||
export { pause }
|
|
||||||
export { seek }
|
|
||||||
export { setVolume }
|
|
||||||
export { setRate }
|
|
||||||
export default {
|
|
||||||
init,
|
|
||||||
toggleMenu,
|
|
||||||
selectDevice,
|
|
||||||
stop,
|
|
||||||
play,
|
|
||||||
pause,
|
|
||||||
seek,
|
|
||||||
setVolume,
|
|
||||||
setRate
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
dispatch,
|
||||||
|
dispatcher,
|
||||||
|
setDispatch
|
||||||
|
}
|
||||||
|
|
||||||
const dispatchers = {}
|
const dispatchers = {}
|
||||||
let _dispatch = () => { }
|
let _dispatch = function () {}
|
||||||
|
|
||||||
function setDispatch (dispatch) {
|
function setDispatch (dispatch) {
|
||||||
_dispatch = dispatch
|
_dispatch = dispatch
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatch (...args) {
|
function dispatch (...args) {
|
||||||
_dispatch(...args)
|
_dispatch(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
|
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
|
||||||
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler
|
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler
|
||||||
// function. This prevents React from updating the listener functions on
|
// function. This prevents React from updating the listener functions on
|
||||||
@@ -14,23 +23,17 @@ function dispatcher (...args) {
|
|||||||
const str = JSON.stringify(args)
|
const str = JSON.stringify(args)
|
||||||
let handler = dispatchers[str]
|
let handler = dispatchers[str]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
handler = dispatchers[str] = e => {
|
handler = dispatchers[str] = function (e) {
|
||||||
// Do not propagate click to elements below the button
|
// Do not propagate click to elements below the button
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
if (e.currentTarget.classList.contains('disabled')) {
|
if (e.currentTarget.classList.contains('disabled')) {
|
||||||
// Ignore clicks on disabled elements
|
// Ignore clicks on disabled elements
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(...args)
|
dispatch(...args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
export { dispatch }
|
|
||||||
export { dispatcher }
|
|
||||||
export { setDispatch }
|
|
||||||
export default {
|
|
||||||
dispatch,
|
|
||||||
dispatcher,
|
|
||||||
setDispatch
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,37 +1,35 @@
|
|||||||
import ExtendableError from 'es6-error'
|
const ExtendableError = require('es6-error')
|
||||||
|
|
||||||
/* Generic errors */
|
/* Generic errors */
|
||||||
class CastingError extends ExtendableError {
|
|
||||||
}
|
class CastingError extends ExtendableError {}
|
||||||
class PlaybackError extends ExtendableError {
|
class PlaybackError extends ExtendableError {}
|
||||||
}
|
class SoundError extends ExtendableError {}
|
||||||
class SoundError extends ExtendableError {
|
class TorrentError extends ExtendableError {}
|
||||||
}
|
|
||||||
class TorrentError extends ExtendableError {
|
|
||||||
}
|
|
||||||
/* Playback */
|
/* Playback */
|
||||||
|
|
||||||
class UnplayableTorrentError extends PlaybackError {
|
class UnplayableTorrentError extends PlaybackError {
|
||||||
constructor () { super('Can\'t play any files in torrent') }
|
constructor () { super('Can\'t play any files in torrent') }
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnplayableFileError extends PlaybackError {
|
class UnplayableFileError extends PlaybackError {
|
||||||
constructor () { super('Can\'t play that file') }
|
constructor () { super('Can\'t play that file') }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sound */
|
/* Sound */
|
||||||
|
|
||||||
class InvalidSoundNameError extends SoundError {
|
class InvalidSoundNameError extends SoundError {
|
||||||
constructor (name) { super(`Invalid sound name: ${name}`) }
|
constructor (name) { super(`Invalid sound name: ${name}`) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Torrent */
|
/* Torrent */
|
||||||
|
|
||||||
class TorrentKeyNotFoundError extends TorrentError {
|
class TorrentKeyNotFoundError extends TorrentError {
|
||||||
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
|
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
|
||||||
}
|
}
|
||||||
export { CastingError }
|
|
||||||
export { PlaybackError }
|
module.exports = {
|
||||||
export { SoundError }
|
|
||||||
export { TorrentError }
|
|
||||||
export { UnplayableTorrentError }
|
|
||||||
export { UnplayableFileError }
|
|
||||||
export { InvalidSoundNameError }
|
|
||||||
export { TorrentKeyNotFoundError }
|
|
||||||
export default {
|
|
||||||
CastingError,
|
CastingError,
|
||||||
PlaybackError,
|
PlaybackError,
|
||||||
SoundError,
|
SoundError,
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
const mediaExtensions = {
|
const mediaExtensions = {
|
||||||
audio: [
|
audio: [
|
||||||
'.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a',
|
'.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a',
|
||||||
'.m2a', '.m4a', '.mpc', '.m4b', '.mka', '.mp2', '.mp3', '.mpc', '.oga',
|
'.m4a', '.m4b', '.mp2', '.mp3', '.mpc', '.oga', '.ogg', '.opus',
|
||||||
'.ogg', '.opus', '.spx', '.wma', '.wav', '.wv', '.wvp'
|
'.spx', '.wma', '.wav', '.wv', '.wvp'],
|
||||||
],
|
|
||||||
video: [
|
video: [
|
||||||
'.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg',
|
'.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg',
|
||||||
'.ogv', '.webm', '.wmv', '.m2ts'
|
'.ogv', '.webm', '.wmv'],
|
||||||
],
|
|
||||||
image: ['.gif', '.jpg', '.jpeg', '.png']
|
image: ['.gif', '.jpg', '.jpeg', '.png']
|
||||||
}
|
}
|
||||||
export default mediaExtensions
|
|
||||||
|
module.exports = mediaExtensions
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import fs, { copyFileSync } from 'fs'
|
|
||||||
import path from 'path'
|
module.exports = {
|
||||||
import semver from 'semver'
|
run
|
||||||
import config from '../../config.js'
|
}
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const semver = require('semver')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
// 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
|
||||||
@@ -23,27 +29,16 @@ function run (state) {
|
|||||||
if (semver.lt(version, '0.17.0')) migrate_0_17_0(saved)
|
if (semver.lt(version, '0.17.0')) migrate_0_17_0(saved)
|
||||||
if (semver.lt(version, '0.17.2')) migrate_0_17_2(saved)
|
if (semver.lt(version, '0.17.2')) migrate_0_17_2(saved)
|
||||||
if (semver.lt(version, '0.21.0')) migrate_0_21_0(saved)
|
if (semver.lt(version, '0.21.0')) migrate_0_21_0(saved)
|
||||||
if (semver.lt(version, '0.22.0')) migrate_0_22_0(saved)
|
|
||||||
|
|
||||||
if (semver.lt(version, config.APP_VERSION)) {
|
|
||||||
installHandlers(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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whenever the app is updated, re-install default handlers if the user has
|
|
||||||
// enabled them.
|
|
||||||
async function installHandlers (saved) {
|
|
||||||
if (saved.prefs.isFileHandler) {
|
|
||||||
const { ipcRenderer } = await import('electron')
|
|
||||||
ipcRenderer.send('setDefaultFileHandler', true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function migrate_0_7_0 (saved) {
|
function migrate_0_7_0 (saved) {
|
||||||
saved.torrents.forEach(ts => {
|
const cpFile = require('cp-file')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
saved.torrents.forEach(function (ts) {
|
||||||
const infoHash = ts.infoHash
|
const infoHash = ts.infoHash
|
||||||
|
|
||||||
// Replace torrentPath with torrentFileName
|
// Replace torrentPath with torrentFileName
|
||||||
@@ -62,7 +57,7 @@ function migrate_0_7_0 (saved) {
|
|||||||
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
// that only runs once
|
// that only runs once
|
||||||
if (src !== dst) copyFileSync(src, dst)
|
if (src !== dst) cpFile.sync(src, dst)
|
||||||
|
|
||||||
delete ts.torrentPath
|
delete ts.torrentPath
|
||||||
ts.torrentFileName = infoHash + '.torrent'
|
ts.torrentFileName = infoHash + '.torrent'
|
||||||
@@ -77,11 +72,12 @@ function migrate_0_7_0 (saved) {
|
|||||||
dst = path.join(config.POSTER_PATH, infoHash + extension)
|
dst = path.join(config.POSTER_PATH, infoHash + extension)
|
||||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
// that only runs once
|
// that only runs once
|
||||||
if (src !== dst) copyFileSync(src, dst)
|
if (src !== dst) cpFile.sync(src, dst)
|
||||||
|
|
||||||
delete ts.posterURL
|
delete ts.posterURL
|
||||||
ts.posterFileName = infoHash + extension
|
ts.posterFileName = infoHash + extension
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix exception caused by incorrect file ordering.
|
// Fix exception caused by incorrect file ordering.
|
||||||
// https://github.com/webtorrent/webtorrent-desktop/pull/604#issuecomment-222805214
|
// https://github.com/webtorrent/webtorrent-desktop/pull/604#issuecomment-222805214
|
||||||
delete ts.defaultPlayFileIndex
|
delete ts.defaultPlayFileIndex
|
||||||
@@ -106,8 +102,8 @@ function migrate_0_11_0 (saved) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrate_0_12_0 (saved) {
|
function migrate_0_12_0 (saved) {
|
||||||
const TorrentSummary = await import('./torrent-summary')
|
const TorrentSummary = require('./torrent-summary')
|
||||||
|
|
||||||
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
|
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
|
||||||
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
|
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
|
||||||
@@ -124,7 +120,7 @@ async function migrate_0_12_0 (saved) {
|
|||||||
'02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent',
|
'02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent',
|
||||||
'3ba219a8634bf7bae3d848192b2da75ae995589d.torrent'
|
'3ba219a8634bf7bae3d848192b2da75ae995589d.torrent'
|
||||||
]
|
]
|
||||||
saved.torrents.forEach(torrentSummary => {
|
saved.torrents.forEach(function (torrentSummary) {
|
||||||
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
|
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
|
||||||
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
|
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
if (!fileOrFolder) return
|
if (!fileOrFolder) return
|
||||||
@@ -138,16 +134,16 @@ async function migrate_0_12_0 (saved) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_14_0 (saved) {
|
function migrate_0_14_0 (saved) {
|
||||||
saved.torrents.forEach(ts => {
|
saved.torrents.forEach(function (ts) {
|
||||||
delete ts.defaultPlayFileIndex
|
delete ts.defaultPlayFileIndex
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_17_0 (saved) {
|
function migrate_0_17_0 (saved) {
|
||||||
// Fix a sad, sad bug that resulted in 100MB+ config.json files
|
// Fix a sad, sad bug that resulted in 100MB+ config.json files
|
||||||
saved.torrents.forEach(ts => {
|
saved.torrents.forEach(function (ts) {
|
||||||
if (!ts.files) return
|
if (!ts.files) return
|
||||||
ts.files.forEach(file => {
|
ts.files.forEach(function (file) {
|
||||||
if (!file.audioInfo || !file.audioInfo.picture) return
|
if (!file.audioInfo || !file.audioInfo.picture) return
|
||||||
// This contained a Buffer, which 30x'd in size when serialized to JSON
|
// This contained a Buffer, which 30x'd in size when serialized to JSON
|
||||||
delete file.audioInfo.picture
|
delete file.audioInfo.picture
|
||||||
@@ -159,13 +155,19 @@ function migrate_0_17_2 (saved) {
|
|||||||
// Remove the trailing dot (.) from the Wired CD torrent name, since
|
// Remove the trailing dot (.) from the Wired CD torrent name, since
|
||||||
// folders/files that end in a trailing dot (.) or space are not deletable from
|
// folders/files that end in a trailing dot (.) or space are not deletable from
|
||||||
// Windows Explorer. See: https://github.com/webtorrent/webtorrent-desktop/issues/905
|
// Windows Explorer. See: https://github.com/webtorrent/webtorrent-desktop/issues/905
|
||||||
|
|
||||||
|
const cpFile = require('cp-file')
|
||||||
|
const rimraf = require('rimraf')
|
||||||
|
|
||||||
const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.'
|
const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.'
|
||||||
const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share'
|
const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share'
|
||||||
|
|
||||||
const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d'
|
const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d'
|
||||||
const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3'
|
const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3'
|
||||||
|
|
||||||
const ts = saved.torrents.find(ts => ts.infoHash === OLD_HASH)
|
const ts = saved.torrents.find((ts) => {
|
||||||
|
return ts.infoHash === OLD_HASH
|
||||||
|
})
|
||||||
|
|
||||||
if (!ts) return // Wired CD torrent does not exist
|
if (!ts) return // Wired CD torrent does not exist
|
||||||
|
|
||||||
@@ -188,8 +190,8 @@ function migrate_0_17_2 (saved) {
|
|||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
ts.posterFileName = NEW_HASH + '.jpg'
|
ts.posterFileName = NEW_HASH + '.jpg'
|
||||||
|
|
||||||
fs.rmdirSync(path.join(config.TORRENT_PATH, ts.torrentFileName))
|
rimraf.sync(path.join(config.TORRENT_PATH, ts.torrentFileName))
|
||||||
copyFileSync(
|
cpFile.sync(
|
||||||
path.join(config.STATIC_PATH, 'wiredCd.torrent'),
|
path.join(config.STATIC_PATH, 'wiredCd.torrent'),
|
||||||
path.join(config.TORRENT_PATH, NEW_HASH + '.torrent')
|
path.join(config.TORRENT_PATH, NEW_HASH + '.torrent')
|
||||||
)
|
)
|
||||||
@@ -212,13 +214,3 @@ function migrate_0_21_0 (saved) {
|
|||||||
saved.prefs.soundNotifications = true
|
saved.prefs.soundNotifications = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_22_0 (saved) {
|
|
||||||
if (saved.prefs.externalPlayerPath == null) {
|
|
||||||
saved.prefs.externalPlayerPath = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
run
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,36 +1,52 @@
|
|||||||
import TorrentSummary from './torrent-summary.js'
|
module.exports = {
|
||||||
import TorrentPlayer from './torrent-player.js'
|
hasNext,
|
||||||
|
getNextIndex,
|
||||||
|
hasPrevious,
|
||||||
|
getPreviousIndex,
|
||||||
|
getCurrentLocalURL
|
||||||
|
}
|
||||||
|
|
||||||
|
const TorrentSummary = require('./torrent-summary')
|
||||||
|
const TorrentPlayer = require('./torrent-player')
|
||||||
|
|
||||||
const cache = {
|
const cache = {
|
||||||
infoHash: null,
|
infoHash: null,
|
||||||
previousIndex: null,
|
previousIndex: null,
|
||||||
currentIndex: null,
|
currentIndex: null,
|
||||||
nextIndex: null
|
nextIndex: null
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasNext (state) {
|
function hasNext (state) {
|
||||||
updateCache(state)
|
updateCache(state)
|
||||||
return cache.nextIndex !== null
|
return cache.nextIndex !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNextIndex (state) {
|
function getNextIndex (state) {
|
||||||
updateCache(state)
|
updateCache(state)
|
||||||
return cache.nextIndex
|
return cache.nextIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasPrevious (state) {
|
function hasPrevious (state) {
|
||||||
updateCache(state)
|
updateCache(state)
|
||||||
return cache.previousIndex !== null
|
return cache.previousIndex !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreviousIndex (state) {
|
function getPreviousIndex (state) {
|
||||||
updateCache(state)
|
updateCache(state)
|
||||||
return cache.previousIndex
|
return cache.previousIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentLocalURL (state) {
|
function getCurrentLocalURL (state) {
|
||||||
return state.server
|
return state.server
|
||||||
? state.server.localURL + '/' + state.playing.fileIndex + '/' +
|
? state.server.localURL + '/' + state.playing.fileIndex + '/' +
|
||||||
encodeURIComponent(state.playing.fileName)
|
encodeURIComponent(state.playing.fileName)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCache (state) {
|
function updateCache (state) {
|
||||||
const infoHash = state.playing.infoHash
|
const infoHash = state.playing.infoHash
|
||||||
const fileIndex = state.playing.fileIndex
|
const fileIndex = state.playing.fileIndex
|
||||||
|
|
||||||
if (infoHash === cache.infoHash) {
|
if (infoHash === cache.infoHash) {
|
||||||
switch (fileIndex) {
|
switch (fileIndex) {
|
||||||
case cache.currentIndex:
|
case cache.currentIndex:
|
||||||
@@ -49,10 +65,12 @@ function updateCache (state) {
|
|||||||
} else {
|
} else {
|
||||||
cache.infoHash = infoHash
|
cache.infoHash = infoHash
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.previousIndex = findPreviousIndex(state)
|
cache.previousIndex = findPreviousIndex(state)
|
||||||
cache.currentIndex = fileIndex
|
cache.currentIndex = fileIndex
|
||||||
cache.nextIndex = findNextIndex(state)
|
cache.nextIndex = findNextIndex(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPreviousIndex (state) {
|
function findPreviousIndex (state) {
|
||||||
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
||||||
for (let i = state.playing.fileIndex - 1; i >= 0; i--) {
|
for (let i = state.playing.fileIndex - 1; i >= 0; i--) {
|
||||||
@@ -60,6 +78,7 @@ function findPreviousIndex (state) {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function findNextIndex (state) {
|
function findNextIndex (state) {
|
||||||
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
||||||
for (let i = state.playing.fileIndex + 1; i < files.length; i++) {
|
for (let i = state.playing.fileIndex + 1; i < files.length; i++) {
|
||||||
@@ -67,15 +86,3 @@ function findNextIndex (state) {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
export { hasNext }
|
|
||||||
export { getNextIndex }
|
|
||||||
export { hasPrevious }
|
|
||||||
export { getPreviousIndex }
|
|
||||||
export { getCurrentLocalURL }
|
|
||||||
export default {
|
|
||||||
hasNext,
|
|
||||||
getNextIndex,
|
|
||||||
hasPrevious,
|
|
||||||
getPreviousIndex,
|
|
||||||
getCurrentLocalURL
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import config from '../src/config.js'
|
module.exports = {
|
||||||
import { InvalidSoundNameError } from './errors.js'
|
init,
|
||||||
import path from 'path'
|
play
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
const { InvalidSoundNameError } = require('./errors')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
const VOLUME = 0.25
|
const VOLUME = 0.25
|
||||||
|
|
||||||
// App state to access the soundNotifications preference
|
// App state to access the soundNotifications preference
|
||||||
let state
|
let state
|
||||||
|
|
||||||
/* Cache of Audio elements, for instant playback */
|
/* Cache of Audio elements, for instant playback */
|
||||||
const cache = {}
|
const cache = {}
|
||||||
|
|
||||||
const 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'),
|
||||||
@@ -40,16 +49,20 @@ const sounds = {
|
|||||||
volume: VOLUME
|
volume: VOLUME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function init (appState) {
|
function init (appState) {
|
||||||
state = appState
|
state = appState
|
||||||
}
|
}
|
||||||
|
|
||||||
function play (name) {
|
function play (name) {
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.saved.prefs.soundNotifications) {
|
if (!state.saved.prefs.soundNotifications) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let audio = cache[name]
|
let audio = cache[name]
|
||||||
if (!audio) {
|
if (!audio) {
|
||||||
const sound = sounds[name]
|
const sound = sounds[name]
|
||||||
@@ -63,9 +76,3 @@ function play (name) {
|
|||||||
audio.currentTime = 0
|
audio.currentTime = 0
|
||||||
audio.play()
|
audio.play()
|
||||||
}
|
}
|
||||||
export { init }
|
|
||||||
export { play }
|
|
||||||
export default {
|
|
||||||
init,
|
|
||||||
play
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,33 +1,30 @@
|
|||||||
import applicationConfig from 'application-config'
|
const appConfig = require('application-config')('WebTorrent')
|
||||||
import path from 'path'
|
const path = require('path')
|
||||||
import { EventEmitter } from 'events'
|
const { EventEmitter } = require('events')
|
||||||
|
|
||||||
import config from '../../config.js'
|
const config = require('../../config')
|
||||||
|
|
||||||
const appConfig = applicationConfig('WebTorrent')
|
|
||||||
const SAVE_DEBOUNCE_INTERVAL = 1000
|
const SAVE_DEBOUNCE_INTERVAL = 1000
|
||||||
|
|
||||||
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||||
|
|
||||||
const State = Object.assign(new EventEmitter(), {
|
const State = module.exports = Object.assign(new EventEmitter(), {
|
||||||
getDefaultPlayState,
|
getDefaultPlayState,
|
||||||
load,
|
load,
|
||||||
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
|
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
|
||||||
async save (...args) {
|
save: function () {
|
||||||
// Perf optimization: Lazy-require debounce (and it's dependencies)
|
// Perf optimization: Lazy-require debounce (and it's dependencies)
|
||||||
const debounce = await import('debounce')
|
const debounce = require('debounce')
|
||||||
// After first State.save() invokation, future calls go straight to the
|
// After first State.save() invokation, future calls go straight to the
|
||||||
// debounced function
|
// debounced function
|
||||||
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
|
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
|
||||||
State.save(...args)
|
State.save(...arguments)
|
||||||
},
|
},
|
||||||
saveImmediate
|
saveImmediate
|
||||||
})
|
})
|
||||||
|
|
||||||
export default State
|
function getDefaultState () {
|
||||||
|
const LocationHistory = require('location-history')
|
||||||
async function getDefaultState () {
|
|
||||||
const { default: LocationHistory } = await import('location-history')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/*
|
/*
|
||||||
@@ -108,26 +105,22 @@ function getDefaultPlayState () {
|
|||||||
selectedIndex: -1, /* current subtitle track */
|
selectedIndex: -1, /* current subtitle track */
|
||||||
showMenu: false /* popover menu, above the video */
|
showMenu: false /* popover menu, above the video */
|
||||||
},
|
},
|
||||||
audioTracks: {
|
|
||||||
tracks: [],
|
|
||||||
selectedIndex: 0, /* current audio track */
|
|
||||||
showMenu: false /* popover menu, above the video */
|
|
||||||
},
|
|
||||||
aspectRatio: 0 /* aspect ratio of the video */
|
aspectRatio: 0 /* aspect ratio of the video */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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 */
|
||||||
async function setupStateSaved () {
|
function setupStateSaved (cb) {
|
||||||
const { copyFileSync, mkdirSync, readFileSync } = await import('fs')
|
const cpFile = require('cp-file')
|
||||||
const { default: parseTorrent } = await import('parse-torrent')
|
const fs = require('fs')
|
||||||
|
const parseTorrent = require('parse-torrent')
|
||||||
|
|
||||||
const saved = {
|
const saved = {
|
||||||
prefs: {
|
prefs: {
|
||||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
||||||
isFileHandler: false,
|
isFileHandler: false,
|
||||||
openExternalPlayer: false,
|
openExternalPlayer: false,
|
||||||
externalPlayerPath: '',
|
externalPlayerPath: null,
|
||||||
startup: false,
|
startup: false,
|
||||||
soundNotifications: true,
|
soundNotifications: true,
|
||||||
autoAddTorrents: false,
|
autoAddTorrents: false,
|
||||||
@@ -139,28 +132,33 @@ async function setupStateSaved () {
|
|||||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Doing several sync calls during first startup is not ideal
|
const tasks = []
|
||||||
mkdirSync(config.POSTER_PATH, { recursive: true })
|
|
||||||
mkdirSync(config.TORRENT_PATH, { recursive: true })
|
|
||||||
|
|
||||||
config.DEFAULT_TORRENTS.forEach((t, i) => {
|
config.DEFAULT_TORRENTS.forEach((t, i) => {
|
||||||
const infoHash = saved.torrents[i].infoHash
|
const infoHash = saved.torrents[i].infoHash
|
||||||
// TODO: Doing several sync calls during first startup is not ideal
|
tasks.push(
|
||||||
copyFileSync(
|
cpFile(
|
||||||
path.join(config.STATIC_PATH, t.posterFileName),
|
path.join(config.STATIC_PATH, t.posterFileName),
|
||||||
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
|
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
copyFileSync(
|
tasks.push(
|
||||||
path.join(config.STATIC_PATH, t.torrentFileName),
|
cpFile(
|
||||||
path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
path.join(config.STATIC_PATH, t.torrentFileName),
|
||||||
|
path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return saved
|
Promise.all(tasks)
|
||||||
|
.then(
|
||||||
|
() => cb(null, saved),
|
||||||
|
err => cb(err)
|
||||||
|
)
|
||||||
|
|
||||||
function createTorrentObject (t) {
|
function createTorrentObject (t) {
|
||||||
// TODO: Doing several sync calls during first startup is not ideal
|
// TODO: Doing several fs.readFileSync calls during first startup is not ideal
|
||||||
const torrent = readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
||||||
const parsedTorrent = parseTorrent(torrent)
|
const parsedTorrent = parseTorrent(torrent)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -205,25 +203,19 @@ function shouldHidePlayerControls () {
|
|||||||
this.playing.playbackRate === 1
|
this.playing.playbackRate === 1
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load (cb) {
|
function load (cb) {
|
||||||
console.log('state load')
|
appConfig.read(function (err, saved) {
|
||||||
let saved = await appConfig.read()
|
if (err || !saved.version) {
|
||||||
|
console.log('Missing config file: Creating new one')
|
||||||
if (!saved || !saved.version) {
|
setupStateSaved(onSavedState)
|
||||||
console.log('Missing config file: Creating new one')
|
} else {
|
||||||
try {
|
onSavedState(null, saved)
|
||||||
saved = await setupStateSaved()
|
|
||||||
} catch (err) {
|
|
||||||
onSavedState(err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
onSavedState(null, saved)
|
function onSavedState (err, saved) {
|
||||||
|
|
||||||
async function onSavedState (err, saved) {
|
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
const state = await getDefaultState()
|
const state = getDefaultState()
|
||||||
state.saved = saved
|
state.saved = saved
|
||||||
|
|
||||||
if (process.type === 'renderer') {
|
if (process.type === 'renderer') {
|
||||||
@@ -232,13 +224,12 @@ async function load (cb) {
|
|||||||
migrations.run(state)
|
migrations.run(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('calling cb')
|
|
||||||
cb(null, state)
|
cb(null, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write state.saved to the JSON state file
|
// Write state.saved to the JSON state file
|
||||||
async function saveImmediate (state, cb) {
|
function saveImmediate (state, cb) {
|
||||||
console.log('Saving state to ' + appConfig.filePath)
|
console.log('Saving state to ' + appConfig.filePath)
|
||||||
|
|
||||||
// Clean up, so that we're not saving any pending state
|
// Clean up, so that we're not saving any pending state
|
||||||
@@ -247,7 +238,7 @@ async function saveImmediate (state, cb) {
|
|||||||
// 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(x => {
|
.map(function (x) {
|
||||||
const torrent = {}
|
const torrent = {}
|
||||||
for (const key in x) {
|
for (const key in x) {
|
||||||
if (key === 'progress' || key === 'torrentKey') {
|
if (key === 'progress' || key === 'torrentKey') {
|
||||||
@@ -261,10 +252,8 @@ async function saveImmediate (state, cb) {
|
|||||||
return torrent
|
return torrent
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
appConfig.write(copy, (err) => {
|
||||||
await appConfig.write(copy)
|
if (err) console.error(err)
|
||||||
State.emit('stateSaved')
|
else State.emit('stateSaved')
|
||||||
} catch (err) {
|
})
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
import remote from '@electron/remote'
|
// Collects anonymous usage stats and uncaught errors
|
||||||
import config from '../../config.js'
|
// Reports back so that we can improve WebTorrent Desktop
|
||||||
|
module.exports = {
|
||||||
|
init,
|
||||||
|
send,
|
||||||
|
logUncaughtError,
|
||||||
|
logPlayAttempt
|
||||||
|
}
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
let telemetry
|
let telemetry
|
||||||
|
|
||||||
async function init (state) {
|
function init (state) {
|
||||||
telemetry = state.saved.telemetry
|
telemetry = state.saved.telemetry
|
||||||
|
|
||||||
// First app run
|
// First app run
|
||||||
if (!telemetry) {
|
if (!telemetry) {
|
||||||
const crypto = await import('crypto')
|
const crypto = require('crypto')
|
||||||
telemetry = state.saved.telemetry = {
|
telemetry = state.saved.telemetry = {
|
||||||
userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID
|
userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID
|
||||||
}
|
}
|
||||||
@@ -16,7 +26,7 @@ async function init (state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function send (state) {
|
function send (state) {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
telemetry.version = config.APP_VERSION
|
telemetry.version = config.APP_VERSION
|
||||||
telemetry.timestamp = now.toISOString()
|
telemetry.timestamp = now.toISOString()
|
||||||
@@ -32,7 +42,7 @@ async function send (state) {
|
|||||||
return reset()
|
return reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
const get = await import('simple-get')
|
const get = require('simple-get')
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
url: config.TELEMETRY_URL,
|
url: config.TELEMETRY_URL,
|
||||||
@@ -40,7 +50,7 @@ async function send (state) {
|
|||||||
json: true
|
json: true
|
||||||
}
|
}
|
||||||
|
|
||||||
get.post(opts, (err, res) => {
|
get.post(opts, function (err, res) {
|
||||||
if (err) return console.error('Error sending telemetry', err)
|
if (err) return console.error('Error sending telemetry', err)
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
|
return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
|
||||||
@@ -64,7 +74,7 @@ function reset () {
|
|||||||
|
|
||||||
// Track screen resolution
|
// Track screen resolution
|
||||||
function getScreenInfo () {
|
function getScreenInfo () {
|
||||||
return remote.screen.getAllDisplays().map((screen) => ({
|
return electron.remote.screen.getAllDisplays().map((screen) => ({
|
||||||
width: screen.size.width,
|
width: screen.size.width,
|
||||||
height: screen.size.height,
|
height: screen.size.height,
|
||||||
scaleFactor: screen.scaleFactor
|
scaleFactor: screen.scaleFactor
|
||||||
@@ -72,8 +82,8 @@ function getScreenInfo () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Track basic system info like OS version and amount of RAM
|
// Track basic system info like OS version and amount of RAM
|
||||||
async function getSystemInfo () {
|
function getSystemInfo () {
|
||||||
const os = await import('os')
|
const os = require('os')
|
||||||
return {
|
return {
|
||||||
osPlatform: process.platform,
|
osPlatform: process.platform,
|
||||||
osRelease: os.type() + ' ' + os.release(),
|
osRelease: os.type() + ' ' + os.release(),
|
||||||
@@ -96,14 +106,15 @@ function getTorrentStats (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First, count torrents & total file size
|
// First, count torrents & total file size
|
||||||
for (const torrent of state.saved.torrents) {
|
for (let i = 0; i < count; i++) {
|
||||||
const stat = byStatus[torrent.status]
|
const t = state.saved.torrents[i]
|
||||||
if (!torrent || !torrent.files || !stat) continue
|
const stat = byStatus[t.status]
|
||||||
|
if (!t || !t.files || !stat) continue
|
||||||
stat.count++
|
stat.count++
|
||||||
|
for (let j = 0; j < t.files.length; j++) {
|
||||||
for (const file of torrent.files) {
|
const f = t.files[j]
|
||||||
if (!file || !file.length) continue
|
if (!f || !f.length) continue
|
||||||
const fileSizeMB = file.length / (1 << 20)
|
const fileSizeMB = f.length / (1 << 20)
|
||||||
sizeMB += fileSizeMB
|
sizeMB += fileSizeMB
|
||||||
stat.sizeMB += fileSizeMB
|
stat.sizeMB += fileSizeMB
|
||||||
}
|
}
|
||||||
@@ -134,7 +145,7 @@ function roundPow2 (n) {
|
|||||||
if (n <= 0) return 0
|
if (n <= 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
|
||||||
const log2 = Math.log(n) / Math.log(2)
|
const log2 = Math.log(n) / Math.log(2)
|
||||||
return 2 ** Math.round(log2)
|
return Math.pow(2, 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
|
||||||
@@ -210,14 +221,3 @@ function logPlayAttempt (result) {
|
|||||||
attempts.total = (attempts.total || 0) + 1
|
attempts.total = (attempts.total || 0) + 1
|
||||||
attempts[result] = (attempts[result] || 0) + 1
|
attempts[result] = (attempts[result] || 0) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export { init }
|
|
||||||
export { send }
|
|
||||||
export { logUncaughtError }
|
|
||||||
export { logPlayAttempt }
|
|
||||||
export default {
|
|
||||||
init,
|
|
||||||
send,
|
|
||||||
logUncaughtError,
|
|
||||||
logPlayAttempt
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
function calculateEta (missing, downloadSpeed) {
|
|
||||||
if (downloadSpeed === 0 || missing === 0) return
|
|
||||||
const rawEta = missing / downloadSpeed
|
|
||||||
const hours = Math.floor(rawEta / 3600) % 24
|
|
||||||
const minutes = Math.floor(rawEta / 60) % 60
|
|
||||||
const seconds = Math.floor(rawEta % 60)
|
|
||||||
// Only display hours and minutes if they are greater than 0 but always
|
|
||||||
// display minutes if hours is being displayed
|
|
||||||
const hoursStr = hours ? hours + ' h' : ''
|
|
||||||
const minutesStr = (hours || minutes) ? minutes + ' min' : ''
|
|
||||||
const secondsStr = seconds + ' s'
|
|
||||||
return `${hoursStr} ${minutesStr} ${secondsStr} remaining`
|
|
||||||
}
|
|
||||||
export { calculateEta }
|
|
||||||
export default {
|
|
||||||
calculateEta
|
|
||||||
}
|
|
||||||
@@ -1,45 +1,4 @@
|
|||||||
import path from 'path'
|
module.exports = {
|
||||||
import mediaExtensions from './media-extensions.js'
|
|
||||||
// Checks whether a fileSummary or file path is audio/video that we can play,
|
|
||||||
// based on the file extension
|
|
||||||
function isPlayable (file) {
|
|
||||||
return isVideo(file) || isAudio(file)
|
|
||||||
}
|
|
||||||
// Checks whether a fileSummary or file path is playable video
|
|
||||||
function isVideo (file) {
|
|
||||||
return mediaExtensions.video.includes(getFileExtension(file))
|
|
||||||
}
|
|
||||||
// Checks whether a fileSummary or file path is playable audio
|
|
||||||
function isAudio (file) {
|
|
||||||
return mediaExtensions.audio.includes(getFileExtension(file))
|
|
||||||
}
|
|
||||||
// Checks if the argument is either:
|
|
||||||
// - a string that's a valid filename ending in .torrent
|
|
||||||
// - a file object where obj.name is ends in .torrent
|
|
||||||
// - a string that's a magnet link (magnet://...)
|
|
||||||
function isTorrent (file) {
|
|
||||||
return isTorrentFile(file) || isMagnetLink(file)
|
|
||||||
}
|
|
||||||
function isTorrentFile (file) {
|
|
||||||
return getFileExtension(file) === '.torrent'
|
|
||||||
}
|
|
||||||
function isMagnetLink (link) {
|
|
||||||
return typeof link === 'string' && /^(stream-)?magnet:/.test(link)
|
|
||||||
}
|
|
||||||
function getFileExtension (file) {
|
|
||||||
const name = typeof file === 'string' ? file : file.name
|
|
||||||
return path.extname(name).toLowerCase()
|
|
||||||
}
|
|
||||||
function isPlayableTorrentSummary (torrentSummary) {
|
|
||||||
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
|
||||||
}
|
|
||||||
export { isPlayable }
|
|
||||||
export { isVideo }
|
|
||||||
export { isAudio }
|
|
||||||
export { isTorrent }
|
|
||||||
export { isMagnetLink }
|
|
||||||
export { isPlayableTorrentSummary }
|
|
||||||
export default {
|
|
||||||
isPlayable,
|
isPlayable,
|
||||||
isVideo,
|
isVideo,
|
||||||
isAudio,
|
isAudio,
|
||||||
@@ -47,3 +6,48 @@ export default {
|
|||||||
isMagnetLink,
|
isMagnetLink,
|
||||||
isPlayableTorrentSummary
|
isPlayableTorrentSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const mediaExtensions = require('./media-extensions')
|
||||||
|
|
||||||
|
// Checks whether a fileSummary or file path is audio/video that we can play,
|
||||||
|
// based on the file extension
|
||||||
|
function isPlayable (file) {
|
||||||
|
return isVideo(file) || isAudio(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether a fileSummary or file path is playable video
|
||||||
|
function isVideo (file) {
|
||||||
|
return mediaExtensions.video.includes(getFileExtension(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether a fileSummary or file path is playable audio
|
||||||
|
function isAudio (file) {
|
||||||
|
return mediaExtensions.audio.includes(getFileExtension(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the argument is either:
|
||||||
|
// - a string that's a valid filename ending in .torrent
|
||||||
|
// - a file object where obj.name is ends in .torrent
|
||||||
|
// - a string that's a magnet link (magnet://...)
|
||||||
|
function isTorrent (file) {
|
||||||
|
return isTorrentFile(file) || isMagnetLink(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTorrentFile (file) {
|
||||||
|
return getFileExtension(file) === '.torrent'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMagnetLink (link) {
|
||||||
|
return typeof link === 'string' && /^(stream-)?magnet:/.test(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileExtension (file) {
|
||||||
|
const name = typeof file === 'string' ? file : file.name
|
||||||
|
return path.extname(name).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlayableTorrentSummary (torrentSummary) {
|
||||||
|
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
import captureFrame from 'capture-frame'
|
module.exports = torrentPoster
|
||||||
import path from 'path'
|
|
||||||
import mediaExtensions from './media-extensions.js'
|
const captureFrame = require('capture-frame')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const mediaExtensions = require('./media-extensions')
|
||||||
|
|
||||||
const msgNoSuitablePoster = 'Cannot generate a poster from any files in the torrent'
|
const msgNoSuitablePoster = 'Cannot generate a poster from any files in the torrent'
|
||||||
|
|
||||||
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
|
||||||
const posterFile = torrent.files.filter(file => /^poster\.(jpg|png|gif)$/.test(file.name))[0]
|
const posterFile = torrent.files.filter(function (file) {
|
||||||
|
return /^poster\.(jpg|png|gif)$/.test(file.name)
|
||||||
|
})[0]
|
||||||
if (posterFile) return extractPoster(posterFile, cb)
|
if (posterFile) return extractPoster(posterFile, cb)
|
||||||
|
|
||||||
// 'score' each media type based on total size present in torrent
|
// 'score' each media type based on total size present in torrent
|
||||||
const bestScore = ['audio', 'video', 'image'].map(mediaType => ({
|
const bestScore = ['audio', 'video', 'image'].map(mediaType => {
|
||||||
type: mediaType,
|
return {
|
||||||
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])
|
type: mediaType,
|
||||||
})).sort((a, b) => b.size - a.size)[0] // sort descending on size
|
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])
|
||||||
|
}
|
||||||
|
}).sort((a, b) => { // sort descending on size
|
||||||
|
return b.size - a.size
|
||||||
|
})[0]
|
||||||
|
|
||||||
if (bestScore.size === 0) {
|
if (bestScore.size === 0) {
|
||||||
// Admit defeat, no video, audio or image had a significant presence
|
// Admit defeat, no video, audio or image had a significant presence
|
||||||
return cb(new Error(msgNoSuitablePoster))
|
return cb(new Error(msgNoSuitablePoster))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Based on which media type is dominant we select the corresponding poster function
|
// Based on which media type is dominant we select the corresponding poster function
|
||||||
switch (bestScore.type) {
|
switch (bestScore.type) {
|
||||||
case 'audio':
|
case 'audio':
|
||||||
@@ -25,6 +39,7 @@ function torrentPoster (torrent, cb) {
|
|||||||
return torrentPosterFromVideo(torrent, cb)
|
return torrentPosterFromVideo(torrent, cb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the total data size of file matching one of the provided extensions
|
* Calculate the total data size of file matching one of the provided extensions
|
||||||
* @param torrent
|
* @param torrent
|
||||||
@@ -36,8 +51,11 @@ function calculateDataLengthByExtension (torrent, extensions) {
|
|||||||
if (files.length === 0) return 0
|
if (files.length === 0) return 0
|
||||||
return files
|
return files
|
||||||
.map(file => file.length)
|
.map(file => file.length)
|
||||||
.reduce((a, b) => a + b)
|
.reduce((a, b) => {
|
||||||
|
return a + b
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the largest file of a given torrent, filtered by provided extension
|
* Get the largest file of a given torrent, filtered by provided extension
|
||||||
* @param torrent Torrent to search in
|
* @param torrent Torrent to search in
|
||||||
@@ -47,13 +65,16 @@ function calculateDataLengthByExtension (torrent, extensions) {
|
|||||||
function getLargestFileByExtension (torrent, extensions) {
|
function getLargestFileByExtension (torrent, extensions) {
|
||||||
const files = filterOnExtension(torrent, extensions)
|
const files = filterOnExtension(torrent, extensions)
|
||||||
if (files.length === 0) return undefined
|
if (files.length === 0) return undefined
|
||||||
return files.reduce((a, b) => a.length > b.length ? a : b)
|
return files.reduce((a, b) => {
|
||||||
|
return a.length > b.length ? a : b
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter file on a list extension, can be used to find al image files
|
* Filter file on a list extension, can be used to find al image files
|
||||||
* @param torrent Torrent to filter files from
|
* @param torrent Torrent to filter files from
|
||||||
* @param extensions File extensions to filter on
|
* @param extensions File extensions to filter on
|
||||||
* @returns {Array} Array of torrent file objects matching one of the given extensions
|
* @returns {number} Array of torrent file objects matching one of the given extensions
|
||||||
*/
|
*/
|
||||||
function filterOnExtension (torrent, extensions) {
|
function filterOnExtension (torrent, extensions) {
|
||||||
return torrent.files.filter(file => {
|
return torrent.files.filter(file => {
|
||||||
@@ -61,6 +82,7 @@ function filterOnExtension (torrent, extensions) {
|
|||||||
return extensions.indexOf(extname) !== -1
|
return extensions.indexOf(extname) !== -1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a score how likely the file is suitable as a poster
|
* Returns a score how likely the file is suitable as a poster
|
||||||
* @param imgFile File object of an image
|
* @param imgFile File object of an image
|
||||||
@@ -76,6 +98,7 @@ function scoreAudioCoverFile (imgFile) {
|
|||||||
back: 20,
|
back: 20,
|
||||||
spectrogram: -80
|
spectrogram: -80
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const keyword in relevanceScore) {
|
for (const keyword in relevanceScore) {
|
||||||
if (fileName === keyword) {
|
if (fileName === keyword) {
|
||||||
return relevanceScore[keyword]
|
return relevanceScore[keyword]
|
||||||
@@ -86,13 +109,18 @@ function scoreAudioCoverFile (imgFile) {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentPosterFromAudio (torrent, cb) {
|
function torrentPosterFromAudio (torrent, cb) {
|
||||||
const imageFiles = filterOnExtension(torrent, mediaExtensions.image)
|
const imageFiles = filterOnExtension(torrent, mediaExtensions.image)
|
||||||
|
|
||||||
if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster))
|
if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster))
|
||||||
const bestCover = imageFiles.map(file => ({
|
|
||||||
file,
|
const bestCover = imageFiles.map(file => {
|
||||||
score: scoreAudioCoverFile(file)
|
return {
|
||||||
})).reduce((a, b) => {
|
file: file,
|
||||||
|
score: scoreAudioCoverFile(file)
|
||||||
|
}
|
||||||
|
}).reduce((a, b) => {
|
||||||
if (a.score > b.score) {
|
if (a.score > b.score) {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
@@ -105,47 +133,61 @@ function torrentPosterFromAudio (torrent, cb) {
|
|||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
})
|
})
|
||||||
|
|
||||||
const extname = path.extname(bestCover.file.name)
|
const extname = path.extname(bestCover.file.name)
|
||||||
bestCover.file.getBuffer((err, buf) => cb(err, buf, extname))
|
bestCover.file.getBuffer((err, buf) => cb(err, buf, extname))
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentPosterFromVideo (torrent, cb) {
|
function torrentPosterFromVideo (torrent, cb) {
|
||||||
const file = getLargestFileByExtension(torrent, mediaExtensions.video)
|
const file = getLargestFileByExtension(torrent, mediaExtensions.video)
|
||||||
|
|
||||||
const index = torrent.files.indexOf(file)
|
const index = torrent.files.indexOf(file)
|
||||||
|
|
||||||
const server = torrent.createServer(0)
|
const server = torrent.createServer(0)
|
||||||
server.listen(0, onListening)
|
server.listen(0, onListening)
|
||||||
|
|
||||||
function onListening () {
|
function onListening () {
|
||||||
const port = server.address().port
|
const port = server.address().port
|
||||||
const url = 'http://localhost:' + port + '/' + index
|
const url = 'http://localhost:' + port + '/' + index
|
||||||
const video = document.createElement('video')
|
const video = document.createElement('video')
|
||||||
video.addEventListener('canplay', onCanPlay)
|
video.addEventListener('canplay', onCanPlay)
|
||||||
|
|
||||||
video.volume = 0
|
video.volume = 0
|
||||||
video.src = url
|
video.src = url
|
||||||
video.play()
|
video.play()
|
||||||
|
|
||||||
function onCanPlay () {
|
function onCanPlay () {
|
||||||
video.removeEventListener('canplay', onCanPlay)
|
video.removeEventListener('canplay', onCanPlay)
|
||||||
video.addEventListener('seeked', onSeeked)
|
video.addEventListener('seeked', onSeeked)
|
||||||
|
|
||||||
video.currentTime = Math.min((video.duration || 600) * 0.03, 60)
|
video.currentTime = Math.min((video.duration || 600) * 0.03, 60)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSeeked () {
|
function onSeeked () {
|
||||||
video.removeEventListener('seeked', onSeeked)
|
video.removeEventListener('seeked', onSeeked)
|
||||||
const frame = captureFrame(video)
|
|
||||||
const buf = frame && frame.image
|
const buf = captureFrame(video)
|
||||||
|
|
||||||
// unload video element
|
// unload video element
|
||||||
video.pause()
|
video.pause()
|
||||||
video.src = ''
|
video.src = ''
|
||||||
video.load()
|
video.load()
|
||||||
|
|
||||||
server.destroy()
|
server.destroy()
|
||||||
|
|
||||||
if (buf.length === 0) return cb(new Error(msgNoSuitablePoster))
|
if (buf.length === 0) return cb(new Error(msgNoSuitablePoster))
|
||||||
|
|
||||||
cb(null, buf, '.jpg')
|
cb(null, buf, '.jpg')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentPosterFromImage (torrent, cb) {
|
function torrentPosterFromImage (torrent, cb) {
|
||||||
const file = getLargestFileByExtension(torrent, mediaExtensions.image)
|
const file = getLargestFileByExtension(torrent, mediaExtensions.image)
|
||||||
extractPoster(file, cb)
|
extractPoster(file, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPoster (file, cb) {
|
function extractPoster (file, cb) {
|
||||||
const extname = path.extname(file.name)
|
const extname = path.extname(file.name)
|
||||||
file.getBuffer((err, buf) => cb(err, buf, extname))
|
file.getBuffer((err, buf) => { return cb(err, buf, extname) })
|
||||||
}
|
}
|
||||||
export default torrentPoster
|
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import path from 'path'
|
module.exports = {
|
||||||
import config from '../src/config.js'
|
getPosterPath,
|
||||||
|
getTorrentPath,
|
||||||
|
getByKey,
|
||||||
|
getTorrentId,
|
||||||
|
getFileOrFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
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
|
||||||
function getTorrentPath (torrentSummary) {
|
function getTorrentPath (torrentSummary) {
|
||||||
if (!torrentSummary || !torrentSummary.torrentFileName) return null
|
if (!torrentSummary || !torrentSummary.torrentFileName) return null
|
||||||
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
|
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expects a torrentSummary
|
// Expects a 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) {
|
||||||
@@ -15,6 +25,7 @@ function getPosterPath (torrentSummary) {
|
|||||||
// 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, '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -25,12 +36,15 @@ function getTorrentId (torrentSummary) {
|
|||||||
return s.magnetURI || s.infoHash
|
return s.magnetURI || s.infoHash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expects a torrentKey or infoHash
|
// Expects a torrentKey or infoHash
|
||||||
// Returns the corresponding torrentSummary, or undefined
|
// Returns the corresponding torrentSummary, or undefined
|
||||||
function getByKey (state, torrentKey) {
|
function getByKey (state, torrentKey) {
|
||||||
if (!torrentKey) return undefined
|
if (!torrentKey) return undefined
|
||||||
return state.saved.torrents.find((x) => x.torrentKey === torrentKey || x.infoHash === torrentKey)
|
return state.saved.torrents.find((x) =>
|
||||||
|
x.torrentKey === torrentKey || x.infoHash === torrentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the path to either the file (in a single-file torrent) or the root
|
// Returns the path to either the file (in a single-file torrent) or the root
|
||||||
// folder (in multi-file torrent)
|
// folder (in multi-file torrent)
|
||||||
// WARNING: assumes that multi-file torrents consist of a SINGLE folder.
|
// WARNING: assumes that multi-file torrents consist of a SINGLE folder.
|
||||||
@@ -42,15 +56,3 @@ function getFileOrFolder (torrentSummary) {
|
|||||||
const dirname = ts.files[0].path.split(path.sep)[0]
|
const dirname = ts.files[0].path.split(path.sep)[0]
|
||||||
return path.join(ts.path, dirname)
|
return path.join(ts.path, dirname)
|
||||||
}
|
}
|
||||||
export { getPosterPath }
|
|
||||||
export { getTorrentPath }
|
|
||||||
export { getByKey }
|
|
||||||
export { getTorrentId }
|
|
||||||
export { getFileOrFolder }
|
|
||||||
export default {
|
|
||||||
getPosterPath,
|
|
||||||
getTorrentPath,
|
|
||||||
getByKey,
|
|
||||||
getTorrentId,
|
|
||||||
getFileOrFolder
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,43 +1,54 @@
|
|||||||
/* eslint-disable import/first */
|
/**
|
||||||
|
* Perf optimization: Hook into require() to modify how certain modules load:
|
||||||
|
*
|
||||||
|
* - `inline-style-prefixer` (used by `material-ui`) takes ~40ms. It is not
|
||||||
|
* actually used because auto-prefixing is disabled with
|
||||||
|
* `darkBaseTheme.userAgent = false`. Return a fake object.
|
||||||
|
*/
|
||||||
|
const Module = require('module')
|
||||||
|
const _require = Module.prototype.require
|
||||||
|
Module.prototype.require = function (id) {
|
||||||
|
if (id === 'inline-style-prefixer') return {}
|
||||||
|
return _require.apply(this, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
// Perf optimization: Start asynchronously read on config file before all the
|
const crashReporter = require('../crash-reporter')
|
||||||
// blocking import() calls below.
|
crashReporter.init()
|
||||||
|
|
||||||
import State from './lib/state'
|
// Perf optimization: Start asynchronously read on config file before all the
|
||||||
|
// blocking require() calls below.
|
||||||
|
|
||||||
|
const State = require('./lib/state')
|
||||||
State.load(onState)
|
State.load(onState)
|
||||||
|
|
||||||
import createGetter from 'fn-getter'
|
const createGetter = require('fn-getter')
|
||||||
import remote from '@electron/remote'
|
const debounce = require('debounce')
|
||||||
import debounce from 'debounce'
|
const dragDrop = require('drag-drop')
|
||||||
import dragDrop from 'drag-drop'
|
const electron = require('electron')
|
||||||
import electron from 'electron'
|
const fs = require('fs')
|
||||||
import fs from 'fs'
|
const React = require('react')
|
||||||
import * as React from 'react'
|
const ReactDOM = require('react-dom')
|
||||||
import * as ReactDOM from 'react-dom'
|
|
||||||
|
|
||||||
import config from '../config.js'
|
const config = require('../config')
|
||||||
import telemetry from './lib/telemetry.js'
|
const telemetry = require('./lib/telemetry')
|
||||||
import sound from './lib/sound.js'
|
const sound = require('./lib/sound')
|
||||||
import TorrentPlayer from './lib/torrent-player.js'
|
const TorrentPlayer = require('./lib/torrent-player')
|
||||||
import App from './pages/app.js'
|
|
||||||
import { setDispatch } from './lib/dispatcher.js'
|
|
||||||
|
|
||||||
import TorrentController from './controllers/torrent-controller.js'
|
// Perf optimization: Needed immediately, so do not lazy load it below
|
||||||
import AudioTracksController from './controllers/audio-tracks-controller.js'
|
const TorrentListController = require('./controllers/torrent-list-controller')
|
||||||
import SubtitlesController from './controllers/subtitles-controller.js'
|
|
||||||
import PrefsController from './controllers/prefs-controller.js'
|
|
||||||
import PlaybackController from './controllers/playback-controller.js'
|
|
||||||
import MediaController from './controllers/media-controller.js'
|
|
||||||
import TorrentListController from './controllers/torrent-list-controller.js'
|
|
||||||
import UpdateController from './controllers/update-controller.js'
|
|
||||||
import FolderWatcherController from './controllers/folder-watcher-controller.js'
|
|
||||||
|
|
||||||
const { clipboard, ipcRenderer } = electron
|
const App = require('./pages/app')
|
||||||
|
|
||||||
|
// Electron apps have two processes: a main process (node) runs first and starts
|
||||||
|
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||||
|
// and this IPC channel receives from and sends messages to the main process
|
||||||
|
const ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
// Yo-yo pattern: state object lives here and percolates down thru all the views.
|
// 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(...)
|
||||||
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
|
||||||
let controllers = null
|
let controllers = null
|
||||||
@@ -72,15 +83,37 @@ function onState (err, _state) {
|
|||||||
|
|
||||||
// Create controllers
|
// Create controllers
|
||||||
controllers = {
|
controllers = {
|
||||||
media: createGetter(() => new MediaController(state)),
|
media: createGetter(() => {
|
||||||
playback: createGetter(() => new PlaybackController(state, config, update)),
|
const MediaController = require('./controllers/media-controller')
|
||||||
prefs: createGetter(() => new PrefsController(state, config)),
|
return new MediaController(state)
|
||||||
subtitles: createGetter(() => new SubtitlesController(state)),
|
}),
|
||||||
audioTracks: createGetter(() => new AudioTracksController(state)),
|
playback: createGetter(() => {
|
||||||
torrent: createGetter(() => new TorrentController(state)),
|
const PlaybackController = require('./controllers/playback-controller')
|
||||||
torrentList: createGetter(() => new TorrentListController(state)),
|
return new PlaybackController(state, config, update)
|
||||||
update: createGetter(() => new UpdateController(state)),
|
}),
|
||||||
folderWatcher: createGetter(() => new FolderWatcherController())
|
prefs: createGetter(() => {
|
||||||
|
const PrefsController = require('./controllers/prefs-controller')
|
||||||
|
return new PrefsController(state, config)
|
||||||
|
}),
|
||||||
|
subtitles: createGetter(() => {
|
||||||
|
const SubtitlesController = require('./controllers/subtitles-controller')
|
||||||
|
return new SubtitlesController(state)
|
||||||
|
}),
|
||||||
|
torrent: createGetter(() => {
|
||||||
|
const TorrentController = require('./controllers/torrent-controller')
|
||||||
|
return new TorrentController(state)
|
||||||
|
}),
|
||||||
|
torrentList: createGetter(() => {
|
||||||
|
return new TorrentListController(state)
|
||||||
|
}),
|
||||||
|
update: createGetter(() => {
|
||||||
|
const UpdateController = require('./controllers/update-controller')
|
||||||
|
return new UpdateController(state)
|
||||||
|
}),
|
||||||
|
folderWatcher: createGetter(() => {
|
||||||
|
const FolderWatcherController = require('./controllers/folder-watcher-controller')
|
||||||
|
return new FolderWatcherController()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add first page to location history
|
// Add first page to location history
|
||||||
@@ -96,10 +129,7 @@ function onState (err, _state) {
|
|||||||
resumeTorrents()
|
resumeTorrents()
|
||||||
|
|
||||||
// Initialize ReactDOM
|
// Initialize ReactDOM
|
||||||
ReactDOM.render(
|
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
|
||||||
<App state={state} ref={elem => { app = elem }} />,
|
|
||||||
document.querySelector('#body')
|
|
||||||
)
|
|
||||||
|
|
||||||
// Calling update() updates the UI given the current state
|
// Calling update() updates the UI given the current state
|
||||||
// Do this at least once a second to give every file in every torrentSummary
|
// Do this at least once a second to give every file in every torrentSummary
|
||||||
@@ -121,11 +151,11 @@ function onState (err, _state) {
|
|||||||
// Add YouTube style hotkey shortcuts
|
// Add YouTube style hotkey shortcuts
|
||||||
window.addEventListener('keydown', onKeydown)
|
window.addEventListener('keydown', onKeydown)
|
||||||
|
|
||||||
const debouncedFullscreenToggle = debounce(() => {
|
const debouncedFullscreenToggle = debounce(function () {
|
||||||
dispatch('toggleFullScreen')
|
dispatch('toggleFullScreen')
|
||||||
}, 1000, true)
|
}, 1000, true)
|
||||||
|
|
||||||
document.addEventListener('wheel', event => {
|
document.addEventListener('wheel', function (event) {
|
||||||
// ctrlKey detects pinch to zoom, http://crbug.com/289887
|
// ctrlKey detects pinch to zoom, http://crbug.com/289887
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -137,7 +167,7 @@ function onState (err, _state) {
|
|||||||
window.addEventListener('focus', onFocus)
|
window.addEventListener('focus', onFocus)
|
||||||
window.addEventListener('blur', onBlur)
|
window.addEventListener('blur', onBlur)
|
||||||
|
|
||||||
if (remote.getCurrentWindow().isVisible()) {
|
if (electron.remote.getCurrentWindow().isVisible()) {
|
||||||
sound.play('STARTUP')
|
sound.play('STARTUP')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,9 +197,9 @@ function delayedInit () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lazily loads Chromecast and Airplay support
|
// Lazily loads Chromecast and Airplay support
|
||||||
async function lazyLoadCast () {
|
function lazyLoadCast () {
|
||||||
if (!Cast) {
|
if (!Cast) {
|
||||||
Cast = (await import('./lib/cast')).default
|
Cast = require('./lib/cast')
|
||||||
Cast.init(state, update) // Search the local network for Chromecast and Airplays
|
Cast.init(state, update) // Search the local network for Chromecast and Airplays
|
||||||
}
|
}
|
||||||
return Cast
|
return Cast
|
||||||
@@ -221,11 +251,6 @@ const dispatchHandlers = {
|
|||||||
controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
|
controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
|
||||||
deleteTorrent: (infoHash, deleteData) =>
|
deleteTorrent: (infoHash, deleteData) =>
|
||||||
controllers.torrentList().deleteTorrent(infoHash, deleteData),
|
controllers.torrentList().deleteTorrent(infoHash, deleteData),
|
||||||
openTorrentListContextMenu: () => onPaste(),
|
|
||||||
confirmDeleteAllTorrents: (deleteData) =>
|
|
||||||
controllers.torrentList().confirmDeleteAllTorrents(deleteData),
|
|
||||||
deleteAllTorrents: (deleteData) =>
|
|
||||||
controllers.torrentList().deleteAllTorrents(deleteData),
|
|
||||||
toggleSelectTorrent: (infoHash) =>
|
toggleSelectTorrent: (infoHash) =>
|
||||||
controllers.torrentList().toggleSelectTorrent(infoHash),
|
controllers.torrentList().toggleSelectTorrent(infoHash),
|
||||||
openTorrentContextMenu: (infoHash) =>
|
openTorrentContextMenu: (infoHash) =>
|
||||||
@@ -244,12 +269,10 @@ const dispatchHandlers = {
|
|||||||
previousTrack: () => controllers.playback().previousTrack(),
|
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),
|
||||||
preview: (x) => controllers.playback().preview(x),
|
|
||||||
clearPreview: () => controllers.playback().clearPreview(),
|
|
||||||
changePlaybackRate: (dir) => controllers.playback().changePlaybackRate(dir),
|
changePlaybackRate: (dir) => controllers.playback().changePlaybackRate(dir),
|
||||||
changeVolume: (delta) => controllers.playback().changeVolume(delta),
|
changeVolume: (delta) => controllers.playback().changeVolume(delta),
|
||||||
setVolume: (vol) => controllers.playback().setVolume(vol),
|
setVolume: (vol) => controllers.playback().setVolume(vol),
|
||||||
openPath: (infoHash, index) => controllers.playback().openPath(infoHash, index),
|
openItem: (infoHash, index) => controllers.playback().openItem(infoHash, index),
|
||||||
|
|
||||||
// Subtitles
|
// Subtitles
|
||||||
openSubtitles: () => controllers.subtitles().openSubtitles(),
|
openSubtitles: () => controllers.subtitles().openSubtitles(),
|
||||||
@@ -258,10 +281,6 @@ 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),
|
||||||
|
|
||||||
// Audio Tracks
|
|
||||||
selectAudioTrack: (index) => controllers.audioTracks().selectAudioTrack(index),
|
|
||||||
toggleAudioTracksMenu: () => controllers.audioTracks().toggleAudioTracksMenu(),
|
|
||||||
|
|
||||||
// Local media: <video>, <audio>, external players
|
// 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),
|
||||||
@@ -281,7 +300,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: checkDownloadPath,
|
||||||
startFolderWatcher: () => controllers.folderWatcher().start(),
|
startFolderWatcher: () => controllers.folderWatcher().start(),
|
||||||
stopFolderWatcher: () => controllers.folderWatcher().stop(),
|
stopFolderWatcher: () => controllers.folderWatcher().stop(),
|
||||||
|
|
||||||
@@ -291,20 +310,20 @@ const dispatchHandlers = {
|
|||||||
|
|
||||||
// Navigation between screens (back, forward, ESC, etc)
|
// Navigation between screens (back, forward, ESC, etc)
|
||||||
exitModal: () => { state.modal = null },
|
exitModal: () => { state.modal = null },
|
||||||
backToList,
|
backToList: backToList,
|
||||||
escapeBack,
|
escapeBack: escapeBack,
|
||||||
back: () => state.location.back(),
|
back: () => state.location.back(),
|
||||||
forward: () => state.location.forward(),
|
forward: () => state.location.forward(),
|
||||||
cancel: () => state.location.cancel(),
|
cancel: () => state.location.cancel(),
|
||||||
|
|
||||||
// Controlling the window
|
// Controlling the window
|
||||||
setDimensions,
|
setDimensions: setDimensions,
|
||||||
toggleFullScreen: (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
|
toggleFullScreen: (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
|
||||||
setTitle: (title) => { state.window.title = title },
|
setTitle: (title) => { state.window.title = title },
|
||||||
resetTitle: () => { state.window.title = config.APP_WINDOW_TITLE },
|
resetTitle: () => { state.window.title = config.APP_WINDOW_TITLE },
|
||||||
|
|
||||||
// Everything else
|
// Everything else
|
||||||
onOpen,
|
onOpen: onOpen,
|
||||||
error: onError,
|
error: onError,
|
||||||
uncaughtError: (proc, err) => telemetry.logUncaughtError(proc, err),
|
uncaughtError: (proc, err) => telemetry.logUncaughtError(proc, err),
|
||||||
stateSave: () => State.save(state),
|
stateSave: () => State.save(state),
|
||||||
@@ -366,7 +385,7 @@ function setupIpc () {
|
|||||||
function backToList () {
|
function backToList () {
|
||||||
// Exit any modals and screens with a back button
|
// Exit any modals and screens with a back button
|
||||||
state.modal = null
|
state.modal = null
|
||||||
state.location.backToFirst(() => {
|
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
|
||||||
const contentTag = document.querySelector('.content')
|
const contentTag = document.querySelector('.content')
|
||||||
if (contentTag) contentTag.scrollTop = 0
|
if (contentTag) contentTag.scrollTop = 0
|
||||||
@@ -400,7 +419,7 @@ function resumeTorrents () {
|
|||||||
// Set window dimensions to match video dimensions or fill the screen
|
// Set window dimensions to match video dimensions or fill the screen
|
||||||
function setDimensions (dimensions) {
|
function setDimensions (dimensions) {
|
||||||
// Don't modify the window size if it's already maximized
|
// Don't modify the window size if it's already maximized
|
||||||
if (remote.getCurrentWindow().isMaximized()) {
|
if (electron.remote.getCurrentWindow().isMaximized()) {
|
||||||
state.window.bounds = null
|
state.window.bounds = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -412,7 +431,7 @@ function setDimensions (dimensions) {
|
|||||||
width: window.outerWidth,
|
width: window.outerWidth,
|
||||||
height: window.outerHeight
|
height: window.outerHeight
|
||||||
}
|
}
|
||||||
state.window.wasMaximized = remote.getCurrentWindow().isMaximized
|
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
|
||||||
|
|
||||||
// Limit window size to screen size
|
// Limit window size to screen size
|
||||||
const screenWidth = window.screen.width
|
const screenWidth = window.screen.width
|
||||||
@@ -486,16 +505,13 @@ function onError (err) {
|
|||||||
const editableHtmlTags = new Set(['input', 'textarea'])
|
const editableHtmlTags = new Set(['input', 'textarea'])
|
||||||
|
|
||||||
function onPaste (e) {
|
function onPaste (e) {
|
||||||
if (e && editableHtmlTags.has(e.target.tagName.toLowerCase())) return
|
if (editableHtmlTags.has(e.target.tagName.toLowerCase())) return
|
||||||
controllers.torrentList().addTorrent(clipboard.readText())
|
controllers.torrentList().addTorrent(electron.clipboard.readText())
|
||||||
|
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeydown (e) {
|
function onKeydown (e) {
|
||||||
// prevent event fire on user input elements
|
|
||||||
if (editableHtmlTags.has(e.target.tagName.toLowerCase())) return
|
|
||||||
|
|
||||||
const key = e.key
|
const key = e.key
|
||||||
|
|
||||||
if (key === 'ArrowLeft') {
|
if (key === 'ArrowLeft') {
|
||||||
@@ -535,7 +551,7 @@ function onBlur () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onVisibilityChange () {
|
function onVisibilityChange () {
|
||||||
state.window.isVisible = !document.hidden
|
state.window.isVisible = !document.webkitHidden
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFullscreenChanged (e, isFullScreen) {
|
function onFullscreenChanged (e, isFullScreen) {
|
||||||
@@ -556,7 +572,7 @@ function onWindowBoundsChanged (e, newBounds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkDownloadPath () {
|
function checkDownloadPath () {
|
||||||
fs.stat(state.saved.prefs.downloadPath, (err, stat) => {
|
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.downloadPathStatus = 'missing'
|
state.downloadPathStatus = 'missing'
|
||||||
return console.error(err)
|
return console.error(err)
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
import * as colors from 'material-ui/styles/colors'
|
const colors = require('material-ui/styles/colors')
|
||||||
import createGetter from 'fn-getter'
|
const createGetter = require('fn-getter')
|
||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import darkBaseTheme$0 from 'material-ui/styles/baseThemes/darkBaseTheme'
|
|
||||||
import getMuiTheme$0 from 'material-ui/styles/getMuiTheme'
|
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
|
||||||
import muiThemeProvider from 'material-ui/styles/MuiThemeProvider'
|
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
|
||||||
import Header from '../components/header.js'
|
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
|
||||||
import TorrentListPage from './torrent-list-page.js'
|
|
||||||
import playerPage from './player-page.js'
|
const Header = require('../components/header')
|
||||||
import createTorrentPage from './create-torrent-page.js'
|
|
||||||
import preferencesPage from './preferences-page.js'
|
// Perf optimization: Needed immediately, so do not lazy load it below
|
||||||
import openTorrentAddressModal from '../components/open-torrent-address-modal.js'
|
const TorrentListPage = require('./torrent-list-page')
|
||||||
import removeTorrentModal from '../components/remove-torrent-modal.js'
|
|
||||||
import updateAvailableModal from '../components/update-available-modal.js'
|
|
||||||
import unsupportedMediaModal from '../components/unsupported-media-modal.js'
|
|
||||||
import deleteAllTorrentsModal from '../components/delete-all-torrents-modal.js'
|
|
||||||
const darkBaseTheme = { default: darkBaseTheme$0 }.default
|
|
||||||
const getMuiTheme = getMuiTheme$0.default
|
|
||||||
const MuiThemeProvider = muiThemeProvider.default
|
|
||||||
const Views = {
|
const Views = {
|
||||||
home: createGetter(() => TorrentListPage),
|
home: createGetter(() => TorrentListPage),
|
||||||
player: createGetter(() => playerPage),
|
player: createGetter(() => require('./player-page')),
|
||||||
'create-torrent': createGetter(() => createTorrentPage),
|
'create-torrent': createGetter(() => require('./create-torrent-page')),
|
||||||
preferences: createGetter(() => preferencesPage)
|
preferences: createGetter(() => require('./preferences-page'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modals = {
|
const Modals = {
|
||||||
'open-torrent-address-modal': createGetter(() => openTorrentAddressModal),
|
'open-torrent-address-modal': createGetter(
|
||||||
'remove-torrent-modal': createGetter(() => removeTorrentModal),
|
() => require('../components/open-torrent-address-modal')
|
||||||
'update-available-modal': createGetter(() => updateAvailableModal),
|
),
|
||||||
'unsupported-media-modal': createGetter(() => unsupportedMediaModal),
|
'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')),
|
||||||
'delete-all-torrents-modal': createGetter(() => deleteAllTorrentsModal)
|
'update-available-modal': createGetter(() => require('../components/update-available-modal')),
|
||||||
|
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontFamily = process.platform === 'win32'
|
const fontFamily = process.platform === 'win32'
|
||||||
@@ -48,7 +43,7 @@ darkBaseTheme.palette.accent3Color = colors.redA100
|
|||||||
let darkMuiTheme
|
let darkMuiTheme
|
||||||
let lightMuiTheme
|
let lightMuiTheme
|
||||||
|
|
||||||
export default class App extends React.Component {
|
class App extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const state = this.props.state
|
const state = this.props.state
|
||||||
|
|
||||||
@@ -89,7 +84,9 @@ export default class App extends React.Component {
|
|||||||
const recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
const recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
||||||
const hasErrors = recentErrors.length > 0
|
const hasErrors = recentErrors.length > 0
|
||||||
|
|
||||||
const errorElems = recentErrors.map((error, i) => <div key={i} className='error'>{error.message}</div>)
|
const errorElems = recentErrors.map(function (error, i) {
|
||||||
|
return (<div key={i} className='error'>{error.message}</div>)
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key='errors'
|
key='errors'
|
||||||
@@ -101,12 +98,12 @@ export default class App extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getModal () {
|
getModal () {
|
||||||
const state = this.props.state
|
const state = this.props.state
|
||||||
if (!state.modal) return
|
if (!state.modal) return
|
||||||
|
|
||||||
if (!lightMuiTheme) {
|
if (!lightMuiTheme) {
|
||||||
const lightBaseTheme = (await import('material-ui/styles/baseThemes/lightBaseTheme')).default
|
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
|
||||||
lightBaseTheme.fontFamily = fontFamily
|
lightBaseTheme.fontFamily = fontFamily
|
||||||
lightBaseTheme.userAgent = false
|
lightBaseTheme.userAgent = false
|
||||||
lightMuiTheme = getMuiTheme(lightBaseTheme)
|
lightMuiTheme = getMuiTheme(lightBaseTheme)
|
||||||
@@ -131,3 +128,5 @@ export default class App extends React.Component {
|
|||||||
return (<View state={state} />)
|
return (<View state={state} />)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = App
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
import createTorrent from 'create-torrent'
|
const createTorrent = require('create-torrent')
|
||||||
import path from 'path'
|
const path = require('path')
|
||||||
import prettyBytes from 'prettier-bytes'
|
const prettyBytes = require('prettier-bytes')
|
||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import flatButton from 'material-ui/FlatButton'
|
|
||||||
import raisedButton from 'material-ui/RaisedButton'
|
|
||||||
import textField from 'material-ui/TextField'
|
|
||||||
import checkbox from 'material-ui/Checkbox'
|
|
||||||
|
|
||||||
import { dispatch, dispatcher } from '../lib/dispatcher.js'
|
const { dispatch, dispatcher } = require('../lib/dispatcher')
|
||||||
import CreateTorrentErrorPage from '../components/create-torrent-error-page.js'
|
|
||||||
import Heading from '../components/heading.js'
|
|
||||||
import ShowMore from '../components/show-more.js'
|
|
||||||
|
|
||||||
const FlatButton = flatButton.default
|
const FlatButton = require('material-ui/FlatButton').default
|
||||||
const RaisedButton = raisedButton.default
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
const TextField = textField.default
|
const TextField = require('material-ui/TextField').default
|
||||||
const Checkbox = checkbox.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.
|
// Shows a basic UI to create a torrent, from an already-selected file or folder.
|
||||||
// Includes a "Show Advanced..." button and more advanced UI.
|
// Includes a "Show Advanced..." button and more advanced UI.
|
||||||
export default class CreateTorrentPage extends React.Component {
|
class CreateTorrentPage extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
@@ -226,3 +223,5 @@ function containsDots (path, pathPrefix) {
|
|||||||
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
|
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
|
||||||
return ('/' + suffix).includes('/.')
|
return ('/' + suffix).includes('/.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = CreateTorrentPage
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import * as React from 'react'
|
/* global HTMLMediaElement */
|
||||||
import bitfield from 'bitfield'
|
|
||||||
import prettyBytes from 'prettier-bytes'
|
|
||||||
|
|
||||||
import TorrentSummary from '../lib/torrent-summary.js'
|
const React = require('react')
|
||||||
import Playlist from '../lib/playlist.js'
|
const Bitfield = require('bitfield')
|
||||||
import { dispatch, dispatcher } from '../lib/dispatcher.js'
|
const prettyBytes = require('prettier-bytes')
|
||||||
import config from '../../config.js'
|
|
||||||
import { calculateEta } from '../lib/time.js'
|
|
||||||
|
|
||||||
const BitField = bitfield.default
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const Playlist = require('../lib/playlist')
|
||||||
|
const { dispatch, dispatcher } = require('../lib/dispatcher')
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
export default class Player extends React.Component {
|
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||||
|
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
|
||||||
@@ -97,31 +97,25 @@ function renderMedia (state) {
|
|||||||
delete file.selectedSubtitle
|
delete file.selectedSubtitle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch to selected audio track
|
|
||||||
const audioTracks = mediaElement.audioTracks || []
|
|
||||||
for (let j = 0; j < audioTracks.length; j++) {
|
|
||||||
const isSelectedTrack = j === state.playing.audioTracks.selectedIndex
|
|
||||||
audioTracks[j].enabled = isSelectedTrack
|
|
||||||
}
|
|
||||||
|
|
||||||
state.playing.volume = mediaElement.volume
|
state.playing.volume = mediaElement.volume
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add subtitles to the <video> tag
|
// Add subtitles to the <video> tag
|
||||||
const trackTags = []
|
const trackTags = []
|
||||||
if (state.playing.subtitles.selectedIndex >= 0) {
|
if (state.playing.subtitles.selectedIndex >= 0) {
|
||||||
state.playing.subtitles.tracks.forEach((track, i) => {
|
for (let i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||||
|
const track = state.playing.subtitles.tracks[i]
|
||||||
const isSelected = state.playing.subtitles.selectedIndex === i
|
const isSelected = state.playing.subtitles.selectedIndex === i
|
||||||
trackTags.push(
|
trackTags.push(
|
||||||
<track
|
<track
|
||||||
key={i}
|
key={i}
|
||||||
default={isSelected}
|
default={isSelected ? 'default' : ''}
|
||||||
label={track.label}
|
label={track.label}
|
||||||
kind='subtitles'
|
type='subtitles'
|
||||||
src={track.buffer}
|
src={track.buffer}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the <audio> or <video> tag
|
// Create the <audio> or <video> tag
|
||||||
@@ -130,13 +124,13 @@ function renderMedia (state) {
|
|||||||
<MediaTagName
|
<MediaTagName
|
||||||
src={Playlist.getCurrentLocalURL(state)}
|
src={Playlist.getCurrentLocalURL(state)}
|
||||||
onDoubleClick={dispatcher('toggleFullScreen')}
|
onDoubleClick={dispatcher('toggleFullScreen')}
|
||||||
onClick={dispatcher('playPause')}
|
|
||||||
onLoadedMetadata={onLoadedMetadata}
|
onLoadedMetadata={onLoadedMetadata}
|
||||||
onEnded={onEnded}
|
onEnded={onEnded}
|
||||||
onStalled={dispatcher('mediaStalled')}
|
onStalled={dispatcher('mediaStalled')}
|
||||||
onError={dispatcher('mediaError')}
|
onError={dispatcher('mediaError')}
|
||||||
onTimeUpdate={dispatcher('mediaTimeUpdate')}
|
onTimeUpdate={dispatcher('mediaTimeUpdate')}
|
||||||
onEncrypted={dispatcher('mediaEncrypted')}
|
onEncrypted={dispatcher('mediaEncrypted')}
|
||||||
|
onCanPlay={onCanPlay}
|
||||||
>
|
>
|
||||||
{trackTags}
|
{trackTags}
|
||||||
</MediaTagName>
|
</MediaTagName>
|
||||||
@@ -154,53 +148,18 @@ function renderMedia (state) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// As soon as we know the video dimensions, resize the window
|
||||||
function onLoadedMetadata (e) {
|
function onLoadedMetadata (e) {
|
||||||
const mediaElement = e.target
|
if (state.playing.type !== 'video') return
|
||||||
|
const video = e.target
|
||||||
// check if we can decode video and audio track
|
const dimensions = {
|
||||||
if (state.playing.type === 'video') {
|
width: video.videoWidth,
|
||||||
if (mediaElement.videoTracks.length === 0) {
|
height: video.videoHeight
|
||||||
dispatch('mediaError', 'Video codec unsupported')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaElement.audioTracks.length === 0) {
|
|
||||||
dispatch('mediaError', 'Audio codec unsupported')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch('mediaSuccess')
|
|
||||||
|
|
||||||
const dimensions = {
|
|
||||||
width: mediaElement.videoWidth,
|
|
||||||
height: mediaElement.videoHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// As soon as we know the video dimensions, resize the window
|
|
||||||
dispatch('setDimensions', dimensions)
|
|
||||||
|
|
||||||
// set audioTracks
|
|
||||||
const tracks = []
|
|
||||||
for (let i = 0; i < mediaElement.audioTracks.length; i++) {
|
|
||||||
tracks.push({
|
|
||||||
label: mediaElement.audioTracks[i].label || `Track ${i + 1}`,
|
|
||||||
language: mediaElement.audioTracks[i].language
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
state.playing.audioTracks.tracks = tracks
|
|
||||||
state.playing.audioTracks.selectedIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we can decode audio track
|
|
||||||
if (state.playing.type === 'audio') {
|
|
||||||
if (mediaElement.audioTracks.length === 0) {
|
|
||||||
dispatch('mediaError', 'Audio codec unsupported')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch('mediaSuccess')
|
|
||||||
}
|
}
|
||||||
|
dispatch('setDimensions', dimensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnded () {
|
function onEnded (e) {
|
||||||
if (Playlist.hasNext(state)) {
|
if (Playlist.hasNext(state)) {
|
||||||
dispatch('nextTrack')
|
dispatch('nextTrack')
|
||||||
} else {
|
} else {
|
||||||
@@ -209,6 +168,20 @@ function renderMedia (state) {
|
|||||||
if (state.window.isFullScreen) dispatch('toggleFullScreen')
|
if (state.window.isFullScreen) dispatch('toggleFullScreen')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onCanPlay (e) {
|
||||||
|
const elem = e.target
|
||||||
|
if (elem.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) return
|
||||||
|
if (state.playing.type === 'video' &&
|
||||||
|
elem.webkitVideoDecodedByteCount === 0) {
|
||||||
|
dispatch('mediaError', 'Video codec unsupported')
|
||||||
|
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
||||||
|
dispatch('mediaError', 'Audio codec unsupported')
|
||||||
|
} else {
|
||||||
|
dispatch('mediaSuccess')
|
||||||
|
elem.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOverlay (state) {
|
function renderOverlay (state) {
|
||||||
@@ -342,13 +315,13 @@ function renderAudioMetadata (state) {
|
|||||||
format.push(fileSummary.audioInfo.format.codec)
|
format.push(fileSummary.audioInfo.format.codec)
|
||||||
}
|
}
|
||||||
if (fileSummary.audioInfo.format.bitrate) {
|
if (fileSummary.audioInfo.format.bitrate) {
|
||||||
format.push(Math.round(fileSummary.audioInfo.format.bitrate / 1000) + ' kbit/s') // 128 kbit/s
|
format.push(Math.round(fileSummary.audioInfo.format.bitrate / 1000) + ' kbps') // 128 kbps
|
||||||
}
|
}
|
||||||
if (fileSummary.audioInfo.format.sampleRate) {
|
if (fileSummary.audioInfo.format.sampleRate) {
|
||||||
format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz')
|
format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz')
|
||||||
}
|
}
|
||||||
if (fileSummary.audioInfo.format.bitsPerSample) {
|
if (fileSummary.audioInfo.format.bitsPerSample) {
|
||||||
format.push(fileSummary.audioInfo.format.bitsPerSample + '-bit')
|
format.push(fileSummary.audioInfo.format.bitsPerSample + ' bit')
|
||||||
}
|
}
|
||||||
if (format.length > 0) {
|
if (format.length > 0) {
|
||||||
elems.push((
|
elems.push((
|
||||||
@@ -429,70 +402,22 @@ function renderCastScreen (state) {
|
|||||||
|
|
||||||
const isStarting = state.playing.location.endsWith('-pending')
|
const isStarting = state.playing.location.endsWith('-pending')
|
||||||
const castName = state.playing.castName
|
const castName = state.playing.castName
|
||||||
const fileName = state.getPlayingFileSummary().name || ''
|
|
||||||
let 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 = ''
|
||||||
|
|
||||||
const prog = state.getPlayingTorrentSummary().progress || {}
|
|
||||||
|
|
||||||
// Show a nice title image, if possible
|
// Show a nice title image, if possible
|
||||||
const style = {
|
const style = {
|
||||||
backgroundImage: cssBackgroundImagePoster(state)
|
backgroundImage: cssBackgroundImagePoster(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEta (total, downloaded) {
|
|
||||||
const missing = (total || 0) - (downloaded || 0)
|
|
||||||
const downloadSpeed = prog.downloadSpeed || 0
|
|
||||||
if (downloadSpeed === 0 || missing === 0) return
|
|
||||||
|
|
||||||
const etaStr = calculateEta(missing, downloadSpeed)
|
|
||||||
|
|
||||||
return (<span>{etaStr}</span>)
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDownloadProgress () {
|
|
||||||
if (!prog.files) return
|
|
||||||
|
|
||||||
const fileProg = prog.files[state.playing.fileIndex]
|
|
||||||
const fileProgress = fileProg.numPiecesPresent / fileProg.numPieces
|
|
||||||
const fileLength = state.getPlayingFileSummary().length
|
|
||||||
const fileDownloaded = fileProgress * fileLength
|
|
||||||
|
|
||||||
const progress = Math.round(100 * fileProgress)
|
|
||||||
const total = prettyBytes(fileLength)
|
|
||||||
const completed = prettyBytes(fileDownloaded)
|
|
||||||
|
|
||||||
const downloadSpeed = prettyBytes(prog.downloadSpeed || 0)
|
|
||||||
const uploadSpeed = prettyBytes(prog.uploadSpeed || 0)
|
|
||||||
|
|
||||||
let sizes
|
|
||||||
if (fileProgress < 1) {
|
|
||||||
sizes = <span> | {completed} / {total}</span>
|
|
||||||
} else {
|
|
||||||
sizes = <span> | {completed}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key='download-progress'>
|
|
||||||
<span className='progress'>{progress}% downloaded {sizes}</span>
|
|
||||||
<br />
|
|
||||||
<span>↓ {downloadSpeed}/s ↑ {uploadSpeed}/s | {prog.numPeers || 0} peer(s)</span>
|
|
||||||
<br />
|
|
||||||
{renderEta(fileLength, fileDownloaded)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='cast' className='letterbox' style={style}>
|
<div key='cast' className='letterbox' style={style}>
|
||||||
<div className='cast-screen'>
|
<div className='cast-screen'>
|
||||||
<i className='icon'>{castIcon}</i>
|
<i className='icon'>{castIcon}</i>
|
||||||
<div key='type' className='cast-type'>{castType}</div>
|
<div key='type' className='cast-type'>{castType}</div>
|
||||||
<div key='status' className='cast-status'>{castStatus}</div>
|
<div key='status' className='cast-status'>{castStatus}</div>
|
||||||
<div key='name' className='name'>{fileName}</div>
|
|
||||||
{renderDownloadProgress()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -504,7 +429,7 @@ function renderCastOptions (state) {
|
|||||||
const { location, devices } = state.devices.castMenu
|
const { location, devices } = state.devices.castMenu
|
||||||
const player = state.devices[location]
|
const player = state.devices[location]
|
||||||
|
|
||||||
const items = devices.map((device, ix) => {
|
const items = devices.map(function (device, ix) {
|
||||||
const isSelected = player.device === device
|
const isSelected = player.device === device
|
||||||
const name = device.name
|
const name = device.name
|
||||||
return (
|
return (
|
||||||
@@ -527,7 +452,7 @@ function renderSubtitleOptions (state) {
|
|||||||
const subtitles = state.playing.subtitles
|
const subtitles = state.playing.subtitles
|
||||||
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||||
|
|
||||||
const items = subtitles.tracks.map((track, ix) => {
|
const items = subtitles.tracks.map(function (track, ix) {
|
||||||
const 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)}>
|
||||||
@@ -550,27 +475,6 @@ function renderSubtitleOptions (state) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAudioTrackOptions (state) {
|
|
||||||
const audioTracks = state.playing.audioTracks
|
|
||||||
if (!audioTracks.tracks.length || !audioTracks.showMenu) return
|
|
||||||
|
|
||||||
const items = audioTracks.tracks.map((track, ix) => {
|
|
||||||
const isSelected = state.playing.audioTracks.selectedIndex === ix
|
|
||||||
return (
|
|
||||||
<li key={ix} onClick={dispatcher('selectAudioTrack', ix)}>
|
|
||||||
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
|
||||||
{track.label}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul key='audio-track-options' className='options-list'>
|
|
||||||
{items}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPlayerControls (state) {
|
function renderPlayerControls (state) {
|
||||||
const positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
const positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||||
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||||
@@ -579,15 +483,10 @@ function renderPlayerControls (state) {
|
|||||||
: state.playing.subtitles.selectedIndex >= 0
|
: state.playing.subtitles.selectedIndex >= 0
|
||||||
? 'active'
|
? 'active'
|
||||||
: ''
|
: ''
|
||||||
const multiAudioClass = state.playing.audioTracks.tracks.length > 1
|
|
||||||
? 'active'
|
|
||||||
: 'disabled'
|
|
||||||
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
|
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
|
||||||
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
|
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
renderPreview(state),
|
|
||||||
|
|
||||||
<div key='playback-bar' className='playback-bar'>
|
<div key='playback-bar' className='playback-bar'>
|
||||||
{renderLoadingBar(state)}
|
{renderLoadingBar(state)}
|
||||||
<div
|
<div
|
||||||
@@ -599,8 +498,6 @@ function renderPlayerControls (state) {
|
|||||||
key='scrub-bar'
|
key='scrub-bar'
|
||||||
className='scrub-bar'
|
className='scrub-bar'
|
||||||
draggable='true'
|
draggable='true'
|
||||||
onMouseMove={handleScrubPreview}
|
|
||||||
onMouseOut={clearPreview}
|
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onClick={handleScrub}
|
onClick={handleScrub}
|
||||||
onDrag={handleScrub}
|
onDrag={handleScrub}
|
||||||
@@ -650,14 +547,6 @@ function renderPlayerControls (state) {
|
|||||||
>
|
>
|
||||||
closed_caption
|
closed_caption
|
||||||
</i>
|
</i>
|
||||||
), (
|
|
||||||
<i
|
|
||||||
key='audio-tracks'
|
|
||||||
className={'icon multi-audio float-right ' + multiAudioClass}
|
|
||||||
onClick={handleAudioTracks}
|
|
||||||
>
|
|
||||||
library_music
|
|
||||||
</i>
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +561,7 @@ function renderPlayerControls (state) {
|
|||||||
airplay: { true: 'airplay', false: 'airplay' },
|
airplay: { true: 'airplay', false: 'airplay' },
|
||||||
dlna: { true: 'tv', false: 'tv' }
|
dlna: { true: 'tv', false: 'tv' }
|
||||||
}
|
}
|
||||||
castTypes.forEach(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?
|
||||||
const isCasting = state.playing.location.startsWith(castType)
|
const isCasting = state.playing.location.startsWith(castType)
|
||||||
const player = state.devices[castType]
|
const player = state.devices[castType]
|
||||||
@@ -709,14 +598,10 @@ function renderPlayerControls (state) {
|
|||||||
// Render volume slider
|
// Render volume slider
|
||||||
const volume = state.playing.volume
|
const volume = state.playing.volume
|
||||||
const volumeIcon = 'volume_' + (
|
const volumeIcon = 'volume_' + (
|
||||||
volume === 0
|
volume === 0 ? 'off'
|
||||||
? 'off'
|
: volume < 0.3 ? 'mute'
|
||||||
: volume < 0.3
|
: volume < 0.6 ? 'down'
|
||||||
? 'mute'
|
: 'up')
|
||||||
: volume < 0.6
|
|
||||||
? 'down'
|
|
||||||
: 'up'
|
|
||||||
)
|
|
||||||
const 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), ' +
|
||||||
@@ -759,31 +644,26 @@ function renderPlayerControls (state) {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyImage = new window.Image(0, 0)
|
return (
|
||||||
emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'
|
<div
|
||||||
|
key='controls' className='controls'
|
||||||
|
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
|
||||||
|
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
|
||||||
|
>
|
||||||
|
{elements}
|
||||||
|
{renderCastOptions(state)}
|
||||||
|
{renderSubtitleOptions(state)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
function handleDragStart (e) {
|
function handleDragStart (e) {
|
||||||
|
// Prevent the cursor from changing, eg to a green + icon on Mac
|
||||||
if (e.dataTransfer) {
|
if (e.dataTransfer) {
|
||||||
const dt = e.dataTransfer
|
const dt = e.dataTransfer
|
||||||
// Prevent the cursor from changing, eg to a green + icon on Mac
|
|
||||||
dt.effectAllowed = 'none'
|
dt.effectAllowed = 'none'
|
||||||
// Prevent ghost image
|
|
||||||
dt.setDragImage(emptyImage, 0, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles a scrub hover (preview another position in the video)
|
|
||||||
function handleScrubPreview (e) {
|
|
||||||
// Only show for videos
|
|
||||||
if (!e.clientX || state.playing.type !== 'video') return
|
|
||||||
dispatch('mediaMouseMoved')
|
|
||||||
dispatch('preview', e.clientX)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearPreview () {
|
|
||||||
if (state.playing.type !== 'video') return
|
|
||||||
dispatch('clearPreview')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles a click or drag to scrub (jump to another position in the video)
|
// Handles a click or drag to scrub (jump to another position in the video)
|
||||||
function handleScrub (e) {
|
function handleScrub (e) {
|
||||||
if (!e.clientX) return
|
if (!e.clientX) return
|
||||||
@@ -795,7 +675,7 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handles volume muting and Unmuting
|
// Handles volume muting and Unmuting
|
||||||
function handleVolumeMute () {
|
function handleVolumeMute (e) {
|
||||||
if (state.playing.volume === 0.0) {
|
if (state.playing.volume === 0.0) {
|
||||||
dispatch('setVolume', 1.0)
|
dispatch('setVolume', 1.0)
|
||||||
} else {
|
} else {
|
||||||
@@ -816,73 +696,6 @@ function renderPlayerControls (state) {
|
|||||||
dispatch('toggleSubtitlesMenu')
|
dispatch('toggleSubtitlesMenu')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAudioTracks () {
|
|
||||||
dispatch('toggleAudioTracksMenu')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key='controls' className='controls'
|
|
||||||
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
|
|
||||||
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
|
|
||||||
>
|
|
||||||
{elements}
|
|
||||||
{renderCastOptions(state)}
|
|
||||||
{renderSubtitleOptions(state)}
|
|
||||||
{renderAudioTrackOptions(state)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPreview (state) {
|
|
||||||
const { previewXCoord = null } = state.playing
|
|
||||||
|
|
||||||
// Calculate time from x-coord as fraction of track width
|
|
||||||
const windowWidth = document.querySelector('body').clientWidth
|
|
||||||
const fraction = previewXCoord / windowWidth
|
|
||||||
const time = fraction * state.playing.duration /* seconds */
|
|
||||||
|
|
||||||
const height = 70
|
|
||||||
let width = 0
|
|
||||||
|
|
||||||
const previewEl = document.querySelector('video#preview')
|
|
||||||
if (previewEl !== null && previewXCoord !== null) {
|
|
||||||
previewEl.currentTime = time
|
|
||||||
|
|
||||||
// Auto adjust width to maintain video aspect ratio
|
|
||||||
width = Math.floor((previewEl.videoWidth / previewEl.videoHeight) * height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center preview window on mouse cursor,
|
|
||||||
// while avoiding falling off the left or right edges
|
|
||||||
const xPos = Math.min(Math.max(previewXCoord - (width / 2), 5), windowWidth - width - 5)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key='preview' style={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 50,
|
|
||||||
left: xPos,
|
|
||||||
display: previewXCoord == null && 'none' // Hide preview when XCoord unset
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ width, height, backgroundColor: 'black' }}>
|
|
||||||
<video
|
|
||||||
src={Playlist.getCurrentLocalURL(state)}
|
|
||||||
id='preview'
|
|
||||||
style={{ border: '1px solid lightgrey', borderRadius: 2 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
textAlign: 'center', margin: 5, textShadow: '0 0 2px rgba(0,0,0,.5)', color: '#eee'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formatTime(time, state.playing.duration)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -904,7 +717,7 @@ function renderLoadingBar (state) {
|
|||||||
const parts = []
|
const parts = []
|
||||||
let lastPiecePresent = false
|
let lastPiecePresent = false
|
||||||
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
||||||
const 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) {
|
||||||
@@ -914,7 +727,7 @@ 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
|
||||||
const loadingBarElems = parts.map((part, i) => {
|
const loadingBarElems = parts.map(function (part, i) {
|
||||||
const 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) + '%'
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import PropTypes from 'prop-types'
|
const PropTypes = require('prop-types')
|
||||||
import * as colors from 'material-ui/styles/colors'
|
|
||||||
import checkbox from 'material-ui/Checkbox'
|
|
||||||
import raisedButton from 'material-ui/RaisedButton'
|
|
||||||
|
|
||||||
import Heading from '../components/heading.js'
|
const colors = require('material-ui/styles/colors')
|
||||||
import PathSelector from '../components/path-selector.js'
|
const Checkbox = require('material-ui/Checkbox').default
|
||||||
import { dispatch } from '../lib/dispatcher.js'
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
import config from '../../config.js'
|
const Heading = require('../components/heading')
|
||||||
|
const PathSelector = require('../components/path-selector')
|
||||||
|
|
||||||
const Checkbox = checkbox.default
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
const RaisedButton = raisedButton.default
|
const config = require('../../config')
|
||||||
|
|
||||||
export default class PreferencesPage extends React.Component {
|
class PreferencesPage extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
@@ -288,3 +286,5 @@ class Preference extends React.Component {
|
|||||||
return (<div style={style}>{this.props.children}</div>)
|
return (<div style={style}>{this.props.children}</div>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = PreferencesPage
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import * as React from 'react'
|
const React = require('react')
|
||||||
import prettyBytes from 'prettier-bytes'
|
const prettyBytes = require('prettier-bytes')
|
||||||
import checkbox from 'material-ui/Checkbox'
|
|
||||||
import linearProgress from 'material-ui/LinearProgress'
|
|
||||||
|
|
||||||
import TorrentSummary from '../lib/torrent-summary.js'
|
const Checkbox = require('material-ui/Checkbox').default
|
||||||
import TorrentPlayer from '../lib/torrent-player.js'
|
const LinearProgress = require('material-ui/LinearProgress').default
|
||||||
import { dispatcher } from '../lib/dispatcher.js'
|
|
||||||
import { calculateEta } from '../lib/time.js'
|
|
||||||
|
|
||||||
const Checkbox = checkbox.default
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const LinearProgress = linearProgress.default
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
|
const { dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
export default class TorrentList extends React.Component {
|
module.exports = class TorrentList extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const state = this.props.state
|
const state = this.props.state
|
||||||
|
|
||||||
@@ -38,11 +35,7 @@ export default class TorrentList extends React.Component {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key='torrent-list' className='torrent-list'>
|
||||||
key='torrent-list'
|
|
||||||
className='torrent-list'
|
|
||||||
onContextMenu={dispatcher('openTorrentListContextMenu')}
|
|
||||||
>
|
|
||||||
{contents}
|
{contents}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -198,9 +191,18 @@ export default class TorrentList extends React.Component {
|
|||||||
const downloadSpeed = prog.downloadSpeed
|
const downloadSpeed = prog.downloadSpeed
|
||||||
if (downloadSpeed === 0 || missing === 0) return
|
if (downloadSpeed === 0 || missing === 0) return
|
||||||
|
|
||||||
const etaStr = calculateEta(missing, downloadSpeed)
|
const rawEta = missing / downloadSpeed
|
||||||
|
const hours = Math.floor(rawEta / 3600) % 24
|
||||||
|
const minutes = Math.floor(rawEta / 60) % 60
|
||||||
|
const seconds = Math.floor(rawEta % 60)
|
||||||
|
|
||||||
return (<span key='eta'>{etaStr}</span>)
|
// Only display hours and minutes if they are greater than 0 but always
|
||||||
|
// display minutes if hours is being displayed
|
||||||
|
const hoursStr = hours ? hours + 'h' : ''
|
||||||
|
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
||||||
|
const secondsStr = seconds + 's'
|
||||||
|
|
||||||
|
return (<span key='eta'>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTorrentStatus () {
|
function renderTorrentStatus () {
|
||||||
@@ -210,9 +212,7 @@ export default class TorrentList extends React.Component {
|
|||||||
else if (torrentSummary.progress.progress === 1) status = 'Not seeding'
|
else if (torrentSummary.progress.progress === 1) status = 'Not seeding'
|
||||||
else status = 'Paused'
|
else status = 'Paused'
|
||||||
} else if (torrentSummary.status === 'downloading') {
|
} else if (torrentSummary.status === 'downloading') {
|
||||||
if (!torrentSummary.progress) status = ''
|
status = 'Downloading'
|
||||||
else if (!torrentSummary.progress.ready) status = 'Verifying'
|
|
||||||
else status = 'Downloading'
|
|
||||||
} else if (torrentSummary.status === 'seeding') {
|
} else if (torrentSummary.status === 'seeding') {
|
||||||
status = 'Seeding'
|
status = 'Seeding'
|
||||||
} else { // torrentSummary.status is 'new' or something unexpected
|
} else { // torrentSummary.status is 'new' or something unexpected
|
||||||
@@ -282,17 +282,10 @@ export default class TorrentList extends React.Component {
|
|||||||
)
|
)
|
||||||
} 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
|
||||||
const sortByName = this.props.state.saved.prefs.sortByName
|
const fileRows = torrentSummary.files
|
||||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
|
|
||||||
let 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 }))
|
||||||
|
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
|
||||||
if (sortByName) {
|
|
||||||
fileRows = fileRows.sort((a, b) => collator.compare(a.file.name, b.file.name))
|
|
||||||
}
|
|
||||||
|
|
||||||
fileRows = fileRows.map((obj) => this.renderFileRow(torrentSummary, obj.file, obj.index))
|
|
||||||
|
|
||||||
filesElement = (
|
filesElement = (
|
||||||
<div key='files' className='files'>
|
<div key='files' className='files'>
|
||||||
@@ -323,7 +316,7 @@ export default class TorrentList extends React.Component {
|
|||||||
torrentSummary.progress.files[index]) {
|
torrentSummary.progress.files[index]) {
|
||||||
const fileProg = torrentSummary.progress.files[index]
|
const fileProg = torrentSummary.progress.files[index]
|
||||||
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
||||||
progress = Math.floor(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
|
||||||
@@ -344,7 +337,7 @@ export default class TorrentList extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
icon = 'description' /* file icon, opens in OS default app */
|
icon = 'description' /* file icon, opens in OS default app */
|
||||||
handleClick = isDone
|
handleClick = isDone
|
||||||
? dispatcher('openPath', infoHash, index)
|
? dispatcher('openItem', infoHash, index)
|
||||||
: (e) => e.stopPropagation() // noop if file is not ready
|
: (e) => e.stopPropagation() // noop if file is not ready
|
||||||
}
|
}
|
||||||
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed
|
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
/* eslint-disable import/first */
|
|
||||||
// To keep the UI snappy, we run WebTorrent in its own hidden window, a separate
|
// To keep the UI snappy, we run WebTorrent in its own hidden window, a separate
|
||||||
// process from the main window.
|
// process from the main window.
|
||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
import crypto from 'crypto'
|
const crypto = require('crypto')
|
||||||
import util from 'util'
|
const deepEqual = require('deep-equal')
|
||||||
import { announceList as defaultAnnounceList } from 'create-torrent'
|
const defaultAnnounceList = require('create-torrent').announceList
|
||||||
import electron from 'electron'
|
const electron = require('electron')
|
||||||
import fs from 'fs'
|
const fs = require('fs')
|
||||||
import * as mm from 'music-metadata'
|
const mkdirp = require('mkdirp')
|
||||||
import networkAddress from 'network-address'
|
const mm = require('music-metadata')
|
||||||
import path from 'path'
|
const networkAddress = require('network-address')
|
||||||
import WebTorrent from 'webtorrent'
|
const path = require('path')
|
||||||
|
const WebTorrent = require('webtorrent')
|
||||||
|
|
||||||
import config from '../config.js'
|
const crashReporter = require('../crash-reporter')
|
||||||
import { TorrentKeyNotFoundError } from './lib/errors.js'
|
const config = require('../config')
|
||||||
import torrentPoster from './lib/torrent-poster.js'
|
const { TorrentKeyNotFoundError } = require('./lib/errors')
|
||||||
|
const torrentPoster = require('./lib/torrent-poster')
|
||||||
|
|
||||||
const { ipcRenderer } = electron
|
// Report when the process crashes
|
||||||
|
crashReporter.init()
|
||||||
|
|
||||||
|
// Send & receive messages from the main window
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
|
||||||
// Force use of webtorrent trackers on all torrents
|
// Force use of webtorrent trackers on all torrents
|
||||||
globalThis.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
||||||
.map((arr) => arr[0])
|
.map((arr) => arr[0])
|
||||||
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
|
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebTorrent version.
|
* WebTorrent version.
|
||||||
*/
|
*/
|
||||||
const VERSION = JSON.parse(fs.readFileSync('package.json').toString()).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.
|
||||||
@@ -68,29 +73,29 @@ init()
|
|||||||
function init () {
|
function init () {
|
||||||
listenToClientEvents()
|
listenToClientEvents()
|
||||||
|
|
||||||
ipcRenderer.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
||||||
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
||||||
ipcRenderer.on('wt-stop-torrenting', (e, infoHash) =>
|
ipc.on('wt-stop-torrenting', (e, infoHash) =>
|
||||||
stopTorrenting(infoHash))
|
stopTorrenting(infoHash))
|
||||||
ipcRenderer.on('wt-create-torrent', (e, torrentKey, options) =>
|
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
|
||||||
createTorrent(torrentKey, options))
|
createTorrent(torrentKey, options))
|
||||||
ipcRenderer.on('wt-save-torrent-file', (e, torrentKey) =>
|
ipc.on('wt-save-torrent-file', (e, torrentKey) =>
|
||||||
saveTorrentFile(torrentKey))
|
saveTorrentFile(torrentKey))
|
||||||
ipcRenderer.on('wt-generate-torrent-poster', (e, torrentKey) =>
|
ipc.on('wt-generate-torrent-poster', (e, torrentKey) =>
|
||||||
generateTorrentPoster(torrentKey))
|
generateTorrentPoster(torrentKey))
|
||||||
ipcRenderer.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
||||||
getAudioMetadata(infoHash, index))
|
getAudioMetadata(infoHash, index))
|
||||||
ipcRenderer.on('wt-start-server', (e, infoHash) =>
|
ipc.on('wt-start-server', (e, infoHash) =>
|
||||||
startServer(infoHash))
|
startServer(infoHash))
|
||||||
ipcRenderer.on('wt-stop-server', () =>
|
ipc.on('wt-stop-server', (e) =>
|
||||||
stopServer())
|
stopServer())
|
||||||
ipcRenderer.on('wt-select-files', (e, infoHash, selections) =>
|
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
||||||
selectFiles(infoHash, selections))
|
selectFiles(infoHash, selections))
|
||||||
|
|
||||||
ipcRenderer.send('ipcReadyWebTorrent')
|
ipc.send('ipcReadyWebTorrent')
|
||||||
|
|
||||||
window.addEventListener('error', (e) =>
|
window.addEventListener('error', (e) =>
|
||||||
ipcRenderer.send('wt-uncaught-error', { message: e.error.message, stack: e.error.stack }),
|
ipc.send('wt-uncaught-error', { message: e.error.message, stack: e.error.stack }),
|
||||||
true)
|
true)
|
||||||
|
|
||||||
setInterval(updateTorrentProgress, 1000)
|
setInterval(updateTorrentProgress, 1000)
|
||||||
@@ -98,8 +103,8 @@ function init () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listenToClientEvents () {
|
function listenToClientEvents () {
|
||||||
client.on('warning', (err) => ipcRenderer.send('wt-warning', null, err.message))
|
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||||
client.on('error', (err) => ipcRenderer.send('wt-error', null, err.message))
|
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
|
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
|
||||||
@@ -108,8 +113,8 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
|
|||||||
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
||||||
|
|
||||||
const torrent = client.add(torrentID, {
|
const torrent = client.add(torrentID, {
|
||||||
path,
|
path: path,
|
||||||
fileModtimes
|
fileModtimes: fileModtimes
|
||||||
})
|
})
|
||||||
torrent.key = torrentKey
|
torrent.key = torrentKey
|
||||||
|
|
||||||
@@ -133,44 +138,44 @@ function createTorrent (torrentKey, options) {
|
|||||||
const torrent = client.seed(paths, options)
|
const torrent = client.seed(paths, options)
|
||||||
torrent.key = torrentKey
|
torrent.key = torrentKey
|
||||||
addTorrentEvents(torrent)
|
addTorrentEvents(torrent)
|
||||||
ipcRenderer.send('wt-new-torrent')
|
ipc.send('wt-new-torrent')
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTorrentEvents (torrent) {
|
function addTorrentEvents (torrent) {
|
||||||
torrent.on('warning', (err) =>
|
torrent.on('warning', (err) =>
|
||||||
ipcRenderer.send('wt-warning', torrent.key, err.message))
|
ipc.send('wt-warning', torrent.key, err.message))
|
||||||
torrent.on('error', (err) =>
|
torrent.on('error', (err) =>
|
||||||
ipcRenderer.send('wt-error', torrent.key, err.message))
|
ipc.send('wt-error', torrent.key, err.message))
|
||||||
torrent.on('infoHash', () =>
|
torrent.on('infoHash', () =>
|
||||||
ipcRenderer.send('wt-parsed', torrent.key, torrent.infoHash, torrent.magnetURI))
|
ipc.send('wt-parsed', torrent.key, torrent.infoHash, torrent.magnetURI))
|
||||||
torrent.on('metadata', torrentMetadata)
|
torrent.on('metadata', torrentMetadata)
|
||||||
torrent.on('ready', torrentReady)
|
torrent.on('ready', torrentReady)
|
||||||
torrent.on('done', torrentDone)
|
torrent.on('done', torrentDone)
|
||||||
|
|
||||||
function torrentMetadata () {
|
function torrentMetadata () {
|
||||||
const info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipcRenderer.send('wt-metadata', torrent.key, info)
|
ipc.send('wt-metadata', torrent.key, info)
|
||||||
|
|
||||||
updateTorrentProgress()
|
updateTorrentProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentReady () {
|
function torrentReady () {
|
||||||
const info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipcRenderer.send('wt-ready', torrent.key, info)
|
ipc.send('wt-ready', torrent.key, info)
|
||||||
ipcRenderer.send('wt-ready-' + torrent.infoHash, torrent.key, info)
|
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
|
||||||
|
|
||||||
updateTorrentProgress()
|
updateTorrentProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentDone () {
|
function torrentDone () {
|
||||||
const info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipcRenderer.send('wt-done', torrent.key, info)
|
ipc.send('wt-done', torrent.key, info)
|
||||||
|
|
||||||
updateTorrentProgress()
|
updateTorrentProgress()
|
||||||
|
|
||||||
torrent.getFileModtimes((err, fileModtimes) => {
|
torrent.getFileModtimes(function (err, fileModtimes) {
|
||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
ipcRenderer.send('wt-file-modtimes', torrent.key, fileModtimes)
|
ipc.send('wt-file-modtimes', torrent.key, fileModtimes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,19 +208,19 @@ function saveTorrentFile (torrentKey) {
|
|||||||
const torrent = getTorrent(torrentKey)
|
const torrent = getTorrent(torrentKey)
|
||||||
const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
|
const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
|
||||||
|
|
||||||
fs.access(torrentPath, fs.constants.R_OK, err => {
|
fs.access(torrentPath, fs.constants.R_OK, function (err) {
|
||||||
const fileName = torrent.infoHash + '.torrent'
|
const fileName = torrent.infoHash + '.torrent'
|
||||||
if (!err) {
|
if (!err) {
|
||||||
// We've already saved the file
|
// We've already saved the file
|
||||||
return ipcRenderer.send('wt-file-saved', torrentKey, fileName)
|
return ipc.send('wt-file-saved', torrentKey, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, save the .torrent file, under the app config folder
|
// Otherwise, save the .torrent file, under the app config folder
|
||||||
fs.mkdir(config.TORRENT_PATH, { recursive: true }, _ => {
|
mkdirp(config.TORRENT_PATH, function (_) {
|
||||||
fs.writeFile(torrentPath, torrent.torrentFile, err => {
|
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||||
console.log('saved torrent file %s', torrentPath)
|
console.log('saved torrent file %s', torrentPath)
|
||||||
return ipcRenderer.send('wt-file-saved', torrentKey, fileName)
|
return ipc.send('wt-file-saved', torrentKey, fileName)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -225,17 +230,17 @@ function saveTorrentFile (torrentKey) {
|
|||||||
// 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) {
|
||||||
const torrent = getTorrent(torrentKey)
|
const torrent = getTorrent(torrentKey)
|
||||||
torrentPoster(torrent, (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.mkdir(config.POSTER_PATH, { recursive: true }, err => {
|
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)
|
||||||
const posterFileName = torrent.infoHash + extension
|
const posterFileName = torrent.infoHash + extension
|
||||||
const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
|
const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
|
||||||
fs.writeFile(posterFilePath, buf, 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
|
||||||
ipcRenderer.send('wt-poster', torrentKey, posterFileName)
|
ipc.send('wt-poster', torrentKey, posterFileName)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -244,23 +249,25 @@ function generateTorrentPoster (torrentKey) {
|
|||||||
function updateTorrentProgress () {
|
function updateTorrentProgress () {
|
||||||
const 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 && util.isDeepStrictEqual(progress, prevProgress)) {
|
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 */
|
||||||
}
|
}
|
||||||
ipcRenderer.send('wt-progress', progress)
|
ipc.send('wt-progress', progress)
|
||||||
prevProgress = progress
|
prevProgress = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTorrentProgress () {
|
function getTorrentProgress () {
|
||||||
// First, track overall progress
|
// First, track overall progress
|
||||||
const progress = client.progress
|
const progress = client.progress
|
||||||
const hasActiveTorrents = client.torrents.some(torrent => torrent.progress !== 1)
|
const hasActiveTorrents = client.torrents.some(function (torrent) {
|
||||||
|
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
|
||||||
const torrentProg = client.torrents.map(torrent => {
|
const torrentProg = client.torrents.map(function (torrent) {
|
||||||
const fileProg = torrent.files && torrent.files.map(file => {
|
const fileProg = torrent.files && torrent.files.map(function (file, index) {
|
||||||
const numPieces = file._endPiece - file._startPiece + 1
|
const numPieces = file._endPiece - file._startPiece + 1
|
||||||
let numPiecesPresent = 0
|
let numPiecesPresent = 0
|
||||||
for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
|
for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
|
||||||
@@ -300,12 +307,12 @@ function startServer (infoHash) {
|
|||||||
else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
|
else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServerFromReadyTorrent (torrent) {
|
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, () => {
|
server.listen(0, function () {
|
||||||
const port = server.address().port
|
const port = server.address().port
|
||||||
const urlSuffix = ':' + port
|
const urlSuffix = ':' + port
|
||||||
const info = {
|
const info = {
|
||||||
@@ -315,8 +322,8 @@ function startServerFromReadyTorrent (torrent) {
|
|||||||
networkAddress: networkAddress()
|
networkAddress: networkAddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcRenderer.send('wt-server-running', info)
|
ipc.send('wt-server-running', info)
|
||||||
ipcRenderer.send('wt-server-' + torrent.infoHash, info)
|
ipc.send('wt-server-' + torrent.infoHash, info)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,31 +341,25 @@ function getAudioMetadata (infoHash, index) {
|
|||||||
|
|
||||||
// Set initial matadata to display the filename first.
|
// Set initial matadata to display the filename first.
|
||||||
const metadata = { title: file.name }
|
const metadata = { title: file.name }
|
||||||
ipcRenderer.send('wt-audio-metadata', infoHash, index, metadata)
|
ipc.send('wt-audio-metadata', infoHash, index, metadata)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
native: false,
|
native: false,
|
||||||
skipCovers: true,
|
skipCovers: true,
|
||||||
fileSize: file.length,
|
fileSize: file.length,
|
||||||
observer: () => {
|
observer: event => {
|
||||||
ipcRenderer.send('wt-audio-metadata', infoHash, index, {
|
ipc.send('wt-audio-metadata', infoHash, index, event.metadata)
|
||||||
common: metadata.common,
|
|
||||||
format: metadata.format
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const onMetadata = file.done
|
const onMetaData = file.done
|
||||||
// If completed; use direct file access
|
// If completed; use direct file access
|
||||||
? mm.parseFile(path.join(torrent.path, file.path), options)
|
? mm.parseFile(path.join(torrent.path, file.path), options)
|
||||||
// otherwise stream
|
// otherwise stream
|
||||||
: mm.parseStream(file.createReadStream(), file.name, options)
|
: mm.parseStream(file.createReadStream(), file.name, options)
|
||||||
|
|
||||||
onMetadata
|
onMetaData
|
||||||
.then(
|
.then(
|
||||||
metadata => {
|
() => console.log(`metadata for file='${file.name}' completed.`),
|
||||||
ipcRenderer.send('wt-audio-metadata', infoHash, index, metadata)
|
|
||||||
console.log(`metadata for file='${file.name}' completed.`)
|
|
||||||
},
|
|
||||||
err => {
|
err => {
|
||||||
console.log(
|
console.log(
|
||||||
`error getting audio metadata for ${infoHash}:${index}`,
|
`error getting audio metadata for ${infoHash}:${index}`,
|
||||||
@@ -385,7 +386,7 @@ function selectFiles (torrentOrInfoHash, selections) {
|
|||||||
// selection with individual selections for each file, so we can
|
// selection with individual selections for each file, so we can
|
||||||
// select/deselect files later on
|
// select/deselect files later on
|
||||||
if (!selections) {
|
if (!selections) {
|
||||||
selections = new Array(torrent.files.length).fill(true)
|
selections = torrent.files.map((x) => true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selections specified incorrectly?
|
// Selections specified incorrectly?
|
||||||
@@ -398,15 +399,15 @@ 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)
|
||||||
selections.forEach((selection, i) => {
|
for (let i = 0; i < selections.length; i++) {
|
||||||
const file = torrent.files[i]
|
const file = torrent.files[i]
|
||||||
if (selection) {
|
if (selections[i]) {
|
||||||
file.select()
|
file.select()
|
||||||
} else {
|
} else {
|
||||||
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
|
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
|
||||||
file.deselect()
|
file.deselect()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets a WebTorrent handle by torrentKey
|
// Gets a WebTorrent handle by torrentKey
|
||||||
@@ -424,7 +425,7 @@ function onError (err) {
|
|||||||
// TODO: remove this once the following bugs are fixed:
|
// TODO: remove this once the following bugs are fixed:
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
|
||||||
// https://github.com/electron/electron/issues/7212
|
// https://github.com/electron/electron/issues/7212
|
||||||
window.testOfflineMode = () => {
|
window.testOfflineMode = function () {
|
||||||
console.log('Test, going OFFLINE')
|
console.log('Test, going OFFLINE')
|
||||||
client = window.client = new WebTorrent({
|
client = window.client = new WebTorrent({
|
||||||
peerId: PEER_ID,
|
peerId: PEER_ID,
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
<p>
|
<p>
|
||||||
Version <script>document.write(require('../package.json').version)</script>
|
Version <script>document.write(require('../package.json').version)</script>
|
||||||
(<script>document.write(require('webtorrent/package.json').version)</script>)
|
(<script>document.write(require('webtorrent/package.json').version)</script>)
|
||||||
(<script>document.write(process.arch)</script>)
|
(<script>document.write(process.arch === 'x64' ? '64-bit' : '32-bit')</script>)
|
||||||
</p>
|
</p>
|
||||||
<p><script>document.write(require('../build/config').APP_COPYRIGHT)</script></p>
|
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,25 +1,26 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
|
<% if (version) { %>Version=<%= version %><% } %>
|
||||||
Name=<%= productName %>
|
Name=<%= productName %>
|
||||||
<% if (genericName) { %>GenericName=<%= genericName %><% } %>
|
<% if (genericName) { %>GenericName=<%= genericName %><% } %>
|
||||||
<% if (description) { %>Comment=<%= description %><% } %>
|
<% if (description) { %>Comment=<%= description %><% } %>
|
||||||
Icon=<%= name %>
|
Icon=<%= name %>
|
||||||
<% if (name) { %>Exec=<%= name %> --no-sandbox %U<% } %><%/*HACK: --no-sandbox fixes an Electron 6 bug. See: #1703*/%>
|
<% if (name) { %>Exec=<%= name %> %U<% } %>
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
|
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
|
||||||
<% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;<% } %>
|
<% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;<% } %>
|
||||||
<% if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;<% } %>
|
<% if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;<% } %>
|
||||||
StartupNotify=true
|
StartupNotify=true
|
||||||
StartupWMClass=<%= productName %>
|
<% if (name) { %>StartupWMClass=<%= name %> <% } %>
|
||||||
|
|
||||||
[Desktop Action CreateNewTorrent]
|
[Desktop Action CreateNewTorrent]
|
||||||
Name=Create New Torrent...
|
Name=Create New Torrent...
|
||||||
<% if (name) { %>Exec=<%= name %> --no-sandbox -n <% } %><%/*HACK: --no-sandbox fixes an Electron 6 bug. See: #1703*/%>
|
<% if (name) { %>Exec=<%= name %> -n <% } %>
|
||||||
|
|
||||||
[Desktop Action OpenTorrentFile]
|
[Desktop Action OpenTorrentFile]
|
||||||
Name=Open Torrent File...
|
Name=Open Torrent File...
|
||||||
<% if (name) { %>Exec=<%= name %> --no-sandbox -o <% } %><%/*HACK: --no-sandbox fixes an Electron 6 bug. See: #1703*/%>
|
<% if (name) { %>Exec=<%= name %> -o <% } %>
|
||||||
|
|
||||||
[Desktop Action OpenTorrentAddress]
|
[Desktop Action OpenTorrentAddress]
|
||||||
Name=Open Torrent Address...
|
Name=Open Torrent Address...
|
||||||
<% if (name) { %>Exec=<%= name %> --no-sandbox -u <% } %><%/*HACK: --no-sandbox fixes an Electron 6 bug. See: #1703*/%>
|
<% if (name) { %>Exec=<%= name %> -u <% } %>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 20 KiB |
@@ -322,10 +322,6 @@ textarea,
|
|||||||
* TORRENT LIST
|
* TORRENT LIST
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.torrent-list {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent {
|
.torrent {
|
||||||
background: linear-gradient(to bottom right, #4B79A1, #283E51);
|
background: linear-gradient(to bottom right, #4B79A1, #283E51);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
@@ -562,13 +558,6 @@ body.drag .app::after {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .name {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 1.5em;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* PLAYER CONTROLS
|
* PLAYER CONTROLS
|
||||||
*/
|
*/
|
||||||
@@ -634,11 +623,6 @@ body.drag .app::after {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls .icon.multi-audio {
|
|
||||||
font-size: 26px;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player .controls .icon.fullscreen {
|
.player .controls .icon.fullscreen {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id='body'></div>
|
<div id='body'></div>
|
||||||
<script type="module" src="../build/renderer/main.js"></script>
|
<script>require('../build/renderer/main.js')</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user