Compare commits

..

1 Commits

Author SHA1 Message Date
Borewit
a445c18979 Show error details in case it is not a 'NotSupportedError' error. 2019-10-07 19:03:14 +02:00
133 changed files with 7219 additions and 22996 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: feross

11
.github/ISSUE_TEMPLATE.md vendored Normal file
View 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?**

View File

@@ -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?**

View File

@@ -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
View 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> ![](https://feross.net/s/cats/14.gif)
requestInfoLabelToAdd: 'need more info'
# ProBot Welcome (https://probot.github.io/apps/welcome/)
newPRWelcomeComment: >
🙌 Thanks for opening this pull request! You're awesome.
<br><br> ![](https://feross.net/s/cats/6.gif)
firstPRMergeComment: >
🎉 Congrats on getting your first pull request landed! <br><br>
![](https://feross.net/s/cats/29.gif)

4
.github/lock.yml vendored Normal file
View 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
View 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> ![](https://feross.net/s/cats/7.gif)

17
.github/stale.yml vendored Normal file
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -2,7 +2,6 @@ node_modules/
build/
dist/
npm-debug.log*
.vscode/
# JetBrains IntelliJ IDEA project files
.idea

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: node_js
node_js:
- lts/*

View File

@@ -39,7 +39,6 @@
- Nuno Campos (nuno.campos@me.com)
- Ebrahim Byagowi (ebrahim@gnu.org)
- Josip Janzic (josip@jjanzic.com)
- Egor Yurtaev (yurtaev.egor@gmail.com)
- Emil Bay (github@tixz.dk)
- Borewit (borewit@users.noreply.github.com)
- greenkeeper[bot] (greenkeeper[bot]@users.noreply.github.com)
@@ -56,7 +55,6 @@
- Dan Flettre (flettre@gmail.com)
- Sibiraj (dev.sibiraj@outlook.com)
- clujin (clujin@gmail.com)
- Sharon Grossman (sharong1337@gmail.com)
- Linus Unnebäck (linus@folkdatorn.se)
- Adrian Tombu (adrian@otso.fr)
- Lucas (5874806+RecoX@users.noreply.github.com)
@@ -66,18 +64,10 @@
- Recox (5874806+RecoX@users.noreply.github.com)
- greenkeeper[bot] (23040076+greenkeeper[bot]@users.noreply.github.com)
- hicom150 (hicom150@gmail.com)
- Дамјан Георгиевски (gdamjan@gmail.com)
- Jimmy Wärting (jimmy@warting.se)
- Julen Garcia Leunda (hicom150@gmail.com)
- 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.

View File

@@ -1,64 +1,6 @@
# WebTorrent Desktop Version History
## v0.24.0 - 2020-08-28
### 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
## v0.21.0 - 2019-08
### Added

View File

@@ -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:
```
$ brew install --cask webtorrent
$ brew cask install webtorrent
```
- Try the (unstable) development version by cloning the Git repository. See the
@@ -174,7 +174,7 @@ React.js (Framework to work with Frontend UI):
https://reactjs.org/docs/getting-started.html
Material UI (React components that implement Google's Material Design.):
https://material-ui.com/getting-started/installation
https://material-ui.com/getting-started
### Privacy

View File

@@ -46,11 +46,6 @@
```
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
```
@@ -74,29 +69,17 @@
**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
been downloaded, even though the files are byte for byte identical. This ensures that the
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)
- 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 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.
See Smoke Tests below for details. Open DevTools
on Windows and Mac, and ensure that the auto updater is running. If the auto updater does not
run, users will successfully auto update to this new version, and then be stuck there forever.
### 4. Ship it
@@ -105,5 +88,25 @@
Create a pull request in [webtorrent.io](https://github.com/webtorrent/webtorrent.io). Update
`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.**
## 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.

View File

@@ -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
View 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!')

View File

@@ -8,6 +8,7 @@ const cp = require('child_process')
const electronPackager = require('electron-packager')
const fs = require('fs')
const minimist = require('minimist')
const mkdirp = require('mkdirp')
const os = require('os')
const path = require('path')
const rimraf = require('rimraf')
@@ -36,7 +37,7 @@ const argv = minimist(process.argv.slice(2), {
})
function build () {
console.log('Installing node_modules...')
console.log('Reinstalling node_modules...')
rimraf.sync(NODE_MODULES_PATH)
cp.execSync('npm ci', { stdio: 'inherit' })
@@ -172,8 +173,8 @@ const linux = {
// Build for Linux.
platform: 'linux',
// Build x64, armv7l, and arm64 binaries.
arch: ['x64', 'armv7l', 'arm64']
// Build x64 and arm64 binaries.
arch: ['x64', 'arm64']
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
}
@@ -266,7 +267,6 @@ function buildDarwin (cb) {
function signApp (cb) {
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:
@@ -282,37 +282,16 @@ function buildDarwin (cb) {
* - Membership in the Apple Developer Program
*/
const signOpts = {
verbose: true,
app: appPath,
platform: 'darwin',
identity: 'Developer ID Application: WebTorrent, LLC (5MAMC8G3L8)',
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'
verbose: true
}
console.log('Mac: Signing app...')
sign(signOpts, function (err) {
if (err) return cb(err)
console.log('Mac: Signed app.')
console.log('Mac: Notarizing app...')
notarize(notarizeOpts).then(
function () {
console.log('Mac: Notarized app.')
cb(null)
},
function (err) {
cb(err)
})
cb(null)
})
}
@@ -451,7 +430,7 @@ function buildWin32 (cb) {
// remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
setupIcon: config.APP_ICON + '.ico',
signWithParams,
signWithParams: signWithParams,
title: config.APP_NAME,
usePackageJson: false,
version: pkg.version
@@ -478,13 +457,13 @@ function buildWin32 (cb) {
console.log('Windows: Creating portable app...')
const portablePath = path.join(filesPath, 'Portable Settings')
fs.mkdirSync(portablePath, { recursive: true })
mkdirp.sync(portablePath)
const downloadsPath = path.join(portablePath, 'Downloads')
fs.mkdirSync(downloadsPath, { recursive: true })
mkdirp.sync(downloadsPath)
const tempPath = path.join(portablePath, 'Temp')
fs.mkdirSync(tempPath, { recursive: true })
mkdirp.sync(tempPath)
const inPath = path.join(DIST_PATH, path.basename(filesPath))
const outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
@@ -545,17 +524,7 @@ function buildLinux (cb) {
},
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'
]
desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs')
}
installer(options).then(

View File

@@ -13,7 +13,6 @@ while (<>) {
next if /(dc\@DCs-MacBook.local)/;
next if /(rolandoguedes\@gmail.com)/;
next if /(grunjol\@users.noreply.github.com)/;
next if /(dependabot)/;
$seen{$_} = push @authors, "- ", $_;
}
END {

View File

@@ -1 +0,0 @@
module.exports = require('electron')

View File

@@ -1 +0,0 @@
import('./build/main/index.js')

1
index.js Normal file
View File

@@ -0,0 +1 @@
require('./build/main')

26142
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,89 +1,76 @@
{
"name": "webtorrent-desktop",
"type": "module",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.24.0",
"version": "0.21.0",
"author": {
"name": "WebTorrent, LLC",
"email": "feross@webtorrent.io",
"url": "https://webtorrent.io"
},
"babel": {
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"useBuiltIns": true
}
]
]
},
"bugs": {
"url": "https://github.com/webtorrent/webtorrent-desktop/issues"
},
"dependencies": {
"@electron/remote": "2.0.1",
"airplayer": "github:webtorrent/airplayer#fix-security",
"application-config": "2.0.0",
"arch": "2.2.0",
"auto-launch": "5.0.5",
"bitfield": "4.0.0",
"capture-frame": "4.0.0",
"chokidar": "3.5.2",
"chromecasts": "1.10.2",
"create-torrent": "5.0.1",
"debounce": "1.2.1",
"dlnacasts": "0.1.0",
"drag-drop": "7.2.0",
"es6-error": "4.1.1",
"fn-getter": "1.0.0",
"iso-639-1": "2.1.9",
"languagedetect": "2.0.0",
"location-history": "1.1.2",
"material-ui": "0.20.2",
"music-metadata": "7.11.4",
"network-address": "1.1.2",
"parse-torrent": "9.1.4",
"prettier-bytes": "1.0.4",
"prop-types": "15.7.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"rimraf": "3.0.2",
"run-parallel": "1.2.0",
"semver": "7.3.5",
"simple-concat": "1.0.1",
"simple-get": "4.0.0",
"srt-to-vtt": "1.1.3",
"vlc-command": "1.2.0",
"webtorrent": "1.5.6",
"winreg": "1.2.4"
"application-config": "^1.0.1",
"arch": "^2.1.1",
"auto-launch": "^5.0.5",
"bitfield": "^3.0.0",
"capture-frame": "^3.0.0",
"chokidar": "^3.0.2",
"chromecasts": "^1.9.1",
"create-torrent": "^4.4.1",
"debounce": "^1.2.0",
"deep-equal": "^1.1.0",
"dlnacasts": "^0.1.0",
"drag-drop": "^5.0.1",
"es6-error": "^4.1.1",
"fn-getter": "^1.0.0",
"iso-639-1": "^2.1.0",
"languagedetect": "^1.2.0",
"location-history": "^1.1.1",
"material-ui": "^0.20.2",
"mkdirp": "^0.5.1",
"music-metadata": "^4.5.3",
"network-address": "^1.1.2",
"parse-torrent": "^7.0.1",
"prettier-bytes": "^1.0.4",
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"rimraf": "^3.0.0",
"run-parallel": "^1.1.9",
"semver": "^6.3.0",
"simple-concat": "^1.0.0",
"simple-get": "^3.0.3",
"srt-to-vtt": "^1.1.3",
"vlc-command": "^1.2.0",
"webtorrent": ">=0.107.16",
"winreg": "^1.2.4"
},
"devDependencies": {
"@babel/cli": "7.15.7",
"@babel/core": "7.15.8",
"@babel/eslint-parser": "7.15.8",
"@babel/plugin-transform-react-jsx": "7.14.9",
"cross-zip": "4.0.0",
"depcheck": "1.4.2",
"electron": "15.2.0",
"electron-notarize": "1.1.1",
"electron-osx-sign": "0.5.0",
"electron-packager": "15.4.0",
"electron-winstaller": "5.0.0",
"gh-release": "6.0.1",
"minimist": "1.2.5",
"nodemon": "2.0.13",
"open": "8.3.0",
"plist": "3.0.4",
"pngjs": "6.0.0",
"run-series": "1.1.9",
"spectron": "15.0.0",
"standard": "16.0.4",
"tape": "5.3.1"
"babel-eslint": "^10.0.3",
"buble": "^0.19.8",
"cross-zip": "^2.1.6",
"depcheck": "^0.8.3",
"electron": "~6.0.9",
"electron-osx-sign": "^0.4.13",
"electron-packager": "^14.0.6",
"electron-winstaller": "^4.0.0",
"gh-release": "^3.5.0",
"minimist": "^1.2.0",
"nodemon": "^1.19.2",
"open": "^6.4.0",
"plist": "^3.0.1",
"pngjs": "^3.4.0",
"run-series": "^1.1.8",
"spectron": "^8.0.0",
"standard": "*",
"tape": "^4.11.0",
"walk-sync": "^2.0.2"
},
"engines": {
"node": "^16.0.0",
"npm": "^7.10.0 || ^8.0.0"
"node": ">=4.0.0"
},
"homepage": "https://webtorrent.io",
"keywords": [
@@ -97,11 +84,11 @@
"webtorrent"
],
"license": "MIT",
"main": "index.cjs",
"main": "index.js",
"optionalDependencies": {
"appdmg": "^0.6.0",
"electron-installer-debian": "^3.1.0",
"electron-installer-redhat": "^3.3.0"
"electron-installer-debian": "^2.0.0",
"electron-installer-redhat": "^2.0.0"
},
"private": true,
"productName": "WebTorrent",
@@ -110,24 +97,19 @@
"url": "git://github.com/webtorrent/webtorrent-desktop.git"
},
"scripts": {
"build": "babel src --out-dir build",
"build": "buble src --target chrome:71 --output build",
"clean": "node ./bin/clean.js",
"gh-release": "gh-release",
"install-system-deps": "brew install fakeroot dpkg rpm",
"open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js",
"start": "npm run build && electron --no-sandbox .",
"test": "standard && depcheck --ignores=standard,@babel/eslint-parser --ignore-dirs=build,dist",
"start": "npm run build && electron .",
"test": "standard && depcheck --ignores=standard,babel-eslint --ignore-dirs=build,dist && node ./bin/extra-lint.js",
"test-integration": "npm run build && node ./test",
"update-authors": "./bin/update-authors.sh",
"watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/"
},
"standard": {
"parser": "@babel/eslint-parser"
},
"renovate": {
"extends": [
"github>webtorrent/renovate-config"
]
"parser": "babel-eslint"
}
}

View File

@@ -1,13 +1,12 @@
import applicationConfig from 'application-config'
import path from 'path'
import arch from 'arch'
import fs from 'fs'
import electron from '../electron.cjs'
const appConfig = require('application-config')('WebTorrent')
const path = require('path')
const electron = require('electron')
const arch = require('arch')
const appConfig = applicationConfig('WebTorrent')
const APP_NAME = 'WebTorrent'
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 PORTABLE_PATH = IS_TEST
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
@@ -18,6 +17,93 @@ const IS_PORTABLE = isPortable()
const UI_HEADER_HEIGHT = 38
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 () {
if (IS_PORTABLE) {
return PORTABLE_PATH
@@ -35,12 +121,16 @@ function getDefaultDownloadPath () {
}
function getPath (key) {
if (!process.versions.electron || process.type !== 'browser') {
if (!process.versions.electron) {
// Node.js process
return ''
} else if (process.type === 'renderer') {
// Electron renderer process
return electron.remote.app.getPath(key)
} else {
// Electron main process
return electron.app.getPath(key)
}
// Electron main process
return electron.app.getPath(key)
}
function isTest () {
@@ -57,6 +147,8 @@ function isPortable () {
return false
}
const fs = require('fs')
try {
// This line throws if the "Portable Settings" folder does not exist, and does
// nothing otherwise.
@@ -82,117 +174,3 @@ function isProduction () {
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
}

View File

@@ -1,13 +1,14 @@
import electron from '../electron.cjs'
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
})
module.exports = {
init
}
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
})
}

View File

@@ -1,6 +1,11 @@
import electron from '../../electron.cjs'
import config from '../config.js'
import log from './log.js'
module.exports = {
init
}
const electron = require('electron')
const config = require('../config')
const log = require('./log')
const ANNOUNCEMENT_URL =
`${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..."
* }
*/
async function init () {
const { default: get } = await import('simple-get')
function init () {
const get = require('simple-get')
get.concat(ANNOUNCEMENT_URL, onResponse)
}
@@ -45,7 +50,7 @@ function onResponse (err, res, data) {
title: data.title,
message: data.message,
detail: data.detail
})
}, noop)
}
export default { init }
function noop () {}

View File

@@ -1,13 +1,20 @@
import electron from '../../electron.cjs'
import log from './log.js'
import * as windows from './windows/index.js'
module.exports = {
openSeedFile,
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.
*/
export function openSeedFile () {
function openSeedFile () {
if (!windows.main.win) return
log('openSeedFile')
const opts = {
@@ -22,18 +29,18 @@ export function openSeedFile () {
* Windows and Linux, open dialogs are for files *or* directories only, not both,
* so this function shows a directory dialog on those platforms.
*/
export function openSeedDirectory () {
function openSeedDirectory () {
if (!windows.main.win) return
log('openSeedDirectory')
const opts = process.platform === 'darwin'
? {
title: 'Select a file or folder for the torrent.',
properties: ['openFile', 'openDirectory']
}
title: 'Select a file or folder for the torrent.',
properties: ['openFile', 'openDirectory']
}
: {
title: 'Select a folder for the torrent.',
properties: ['openDirectory']
}
title: 'Select a folder for the torrent.',
properties: ['openDirectory']
}
showOpenSeed(opts)
}
@@ -41,29 +48,30 @@ export function openSeedDirectory () {
* 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.
*/
export function openFiles () {
function openFiles () {
if (!windows.main.win) return
log('openFiles')
const opts = process.platform === 'darwin'
? {
title: 'Select a file or folder to add.',
properties: ['openFile', 'openDirectory']
}
title: 'Select a file or folder to add.',
properties: ['openFile', 'openDirectory']
}
: {
title: 'Select a file to add.',
properties: ['openFile']
}
title: 'Select a file to add.',
properties: ['openFile']
}
setTitle(opts.title)
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('onOpen', selectedPaths)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('onOpen', selectedPaths)
})
}
/*
* Show open dialog to open a .torrent file.
*/
export function openTorrentFile () {
function openTorrentFile () {
if (!windows.main.win) return
log('openTorrentFile')
const opts = {
@@ -72,18 +80,19 @@ export function openTorrentFile () {
properties: ['openFile', 'multiSelections']
}
setTitle(opts.title)
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle()
if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(selectedPath => {
windows.main.dispatch('addTorrent', selectedPath)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(function (selectedPath) {
windows.main.dispatch('addTorrent', selectedPath)
})
})
}
/*
* Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.)
*/
export function openTorrentAddress () {
function openTorrentAddress () {
log('openTorrentAddress')
windows.main.dispatch('openTorrentAddress')
}
@@ -107,8 +116,9 @@ function resetTitle () {
*/
function showOpenSeed (opts) {
setTitle(opts.title)
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
}

View File

@@ -1,8 +1,13 @@
import electron from '../../electron.cjs'
import * as dialog from './dialog.js'
import log from './log.js'
module.exports = {
downloadFinished,
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)
@@ -29,7 +34,7 @@ function setBadge (count) {
if (process.platform === 'darwin' ||
(process.platform === 'linux' && app.isUnityRunning())) {
log(`setBadge: ${count}`)
app.badgeCount = Number(count)
app.setBadgeCount(Number(count))
}
}
@@ -52,5 +57,3 @@ function getMenuTemplate () {
}
]
}
export default { downloadFinished, init, setBadge }

View File

@@ -1,20 +1,28 @@
import cp from 'child_process'
import path from 'path'
import vlcCommand from 'vlc-command'
import log from './log.js'
import * as windows from './windows'
module.exports = {
spawn,
kill,
checkInstall
}
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
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
// otherwise assume the player is installed
if (!playerPath) return vlcCommand(cb)
if (playerPath == null) return vlcCommand(cb)
process.nextTick(() => cb(null))
}
export function spawn (playerPath, url, title) {
if (playerPath) return spawnExternal(playerPath, [url])
function spawn (playerPath, url, title) {
if (playerPath != null) return spawnExternal(playerPath, [url])
// Try to find and use VLC if external player is not specified
vlcCommand((err, vlcPath) => {
@@ -29,7 +37,7 @@ export function spawn (playerPath, url, title) {
})
}
export function kill () {
function kill () {
if (!proc) return
log(`Killing external player, pid ${proc.pid}`)
proc.kill('SIGKILL') // kill -9

View File

@@ -1,7 +1,7 @@
import * as chokidar from 'chokidar'
import log from './log.js'
const chokidar = require('chokidar')
const log = require('./log')
export class FolderWatcher {
class FolderWatcher {
constructor ({ window, state }) {
this.window = window
this.state = state
@@ -46,3 +46,5 @@ export class FolderWatcher {
this.watching = false
}
}
module.exports = FolderWatcher

View File

@@ -1,7 +1,12 @@
import config from '../config.js'
import path from 'path'
module.exports = {
install,
uninstall
}
export function install () {
const config = require('../config')
const path = require('path')
function install () {
switch (process.platform) {
case 'darwin': installDarwin()
break
@@ -10,7 +15,7 @@ export function install () {
}
}
export function uninstall () {
function uninstall () {
switch (process.platform) {
case 'darwin': uninstallDarwin()
break
@@ -19,8 +24,10 @@ export function uninstall () {
}
}
async function installDarwin () {
const { app } = await import('electron')
function installDarwin () {
const electron = require('electron')
const app = electron.app
// On Mac, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime.
app.setAsDefaultProtocolClient('magnet')
@@ -37,14 +44,33 @@ if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH)
}
async function installWin32 () {
const Registry = await import('wingreg')
const log = await import('./log')
function installWin32 () {
const Registry = require('winreg')
const iconPath = path.join(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)
const log = require('./log')
const iconPath = path.join(
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:
@@ -172,8 +198,9 @@ async function installWin32 () {
}
}
async function uninstallWin32 () {
const Registry = await import('winreg')
function uninstallWin32 () {
const Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)

View File

@@ -1,25 +1,19 @@
/* eslint-disable import/first */
console.time('init')
import * as RemoteMain from '@electron/remote/main/index.js'
RemoteMain.initialize()
import electron from '../../electron.cjs'
const { app, ipcMain } = electron
const electron = require('electron')
const app = electron.app
// Start crash reporter early, so it takes effect for child processes
import crashReporter from '../crash-reporter.js'
crashReporter.init()
const parallel = require('run-parallel')
import fs from 'fs'
import parallel from 'run-parallel'
import config from '../config.js'
import ipc from './ipc.js'
import log from './log.js'
import menu from './menu.js'
import State from '../renderer/lib/state.js'
import * as windows from './windows/index.js'
const config = require('../config')
const crashReporter = require('../crash-reporter')
const ipc = require('./ipc')
const log = require('./log')
const menu = require('./menu')
const State = require('../renderer/lib/state')
const windows = require('./windows')
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 argv = sliceArgv(process.argv)
@@ -57,30 +51,27 @@ if (!shouldQuit && !config.IS_PORTABLE) {
if (shouldQuit) {
app.quit()
} else {
init().catch(console.error)
init()
}
async function init () {
console.log('index init')
app.whenReady().then(() => {
console.log('readyyyy')
})
function init () {
app.on('second-instance', (event, commandLine, workingDirectory) => onAppOpen(commandLine))
if (config.IS_PORTABLE) {
console.log('is portable')
const path = await import('path')
const path = require('path')
// Put all user data into the "Portable Settings" folder
app.setPath('userData', config.CONFIG_PATH)
// Put Electron crash files, etc. into the "Portable Settings\Temp" folder
app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp'))
}
const ipcMain = electron.ipcMain
let isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false
parallel({
appReady: (cb) => app.whenReady().then(cb),
appReady: (cb) => app.on('ready', () => cb(null)),
state: (cb) => State.load(cb)
}, onReady)
@@ -90,9 +81,9 @@ async function init () {
isReady = true
const state = results.state
menu.init()
windows.main.init(state, { hidden })
windows.main.init(state, { hidden: hidden })
windows.webtorrent.init()
menu.init()
// To keep app startup fast, some code is delayed.
setTimeout(() => {
@@ -118,13 +109,17 @@ async function init () {
ipc.init()
app.once('ipcReady', () => {
app.once('will-finish-launching', function () {
crashReporter.init()
})
app.once('ipcReady', function () {
log('Command line args:', argv)
processArgv(argv)
console.timeEnd('init')
})
app.on('before-quit', e => {
app.on('before-quit', function (e) {
if (app.isQuitting) return
app.isQuitting = true
@@ -137,19 +132,18 @@ async function init () {
}, 4000) // quit after 4 secs, at most
})
app.on('activate', () => {
console.log('activate')
app.on('activate', function () {
if (isReady) windows.main.show()
})
}
async function delayedInit (state) {
function delayedInit (state) {
if (app.isQuitting) return
const { default: announcement } = await import('./announcement.js')
const { default: dock } = await import('./dock.js')
const { default: updater } = await import('./updater.js')
const { FolderWatcher } = await import('./folder-watcher.js')
const announcement = require('./announcement')
const dock = require('./dock')
const updater = require('./updater')
const FolderWatcher = require('./folder-watcher')
const folderWatcher = new FolderWatcher({ window: windows.main, state })
announcement.init()
@@ -162,13 +156,13 @@ async function delayedInit (state) {
}
if (process.platform === 'win32') {
const userTasks = await import('./user-tasks.js')
const userTasks = require('./user-tasks')
userTasks.init()
}
if (process.platform !== 'darwin') {
const { init: trayInit } = await import('./tray.js')
trayInit()
const tray = require('./tray')
tray.init()
}
}
@@ -205,21 +199,17 @@ function onAppOpen (newArgv) {
// Development: 2 args, eg: electron .
// Test: 4 args, eg: electron -r .../mocks.js .
function sliceArgv (argv) {
return argv.slice(
config.IS_PRODUCTION
? 1
: config.IS_TEST
? 4
: 2
)
return argv.slice(config.IS_PRODUCTION ? 1
: config.IS_TEST ? 4
: 2)
}
async function processArgv (argv) {
function processArgv (argv) {
const torrentIds = []
await Promise.all(argv.forEach(async arg => {
argv.forEach(function (arg) {
if (arg === '-n' || arg === '-o' || arg === '-u') {
// Critical path: Only load the 'dialog' package if it is needed
const dialog = await import('./dialog')
const dialog = require('./dialog')
if (arg === '-n') {
dialog.openSeedDirectory()
} else if (arg === '-o') {
@@ -242,7 +232,7 @@ async function processArgv (argv) {
// running.
torrentIds.push(arg)
}
}))
})
if (torrentIds.length > 0) {
windows.main.dispatch('onOpen', torrentIds)
}

View File

@@ -1,10 +1,15 @@
import electron from '../../electron.cjs'
module.exports = {
init,
setModule
}
import log from './log.js'
import menu from './menu.js'
import * as windows from './windows/index.js'
const electron = require('electron')
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
const messageQueueMainToWebTorrent = []
@@ -18,16 +23,18 @@ function setModule (name, module) {
}
function init () {
ipcMain.once('ipcReady', e => {
const ipc = electron.ipcMain
ipc.once('ipcReady', function (e) {
app.ipcReady = true
app.emit('ipcReady')
})
ipcMain.once('ipcReadyWebTorrent', e => {
ipc.once('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window',
messageQueueMainToWebTorrent.length)
messageQueueMainToWebTorrent.forEach(message => {
messageQueueMainToWebTorrent.forEach(function (message) {
windows.webtorrent.send(message.name, ...message.args)
log('webtorrent: sent queued %s', message.name)
})
@@ -37,12 +44,12 @@ function init () {
* Dialog
*/
ipcMain.on('openTorrentFile', async () => {
const dialog = await import('./dialog')
ipc.on('openTorrentFile', () => {
const dialog = require('./dialog')
dialog.openTorrentFile()
})
ipcMain.on('openFiles', async () => {
const dialog = await import('./dialog')
ipc.on('openFiles', () => {
const dialog = require('./dialog')
dialog.openFiles()
})
@@ -50,12 +57,12 @@ function init () {
* Dock
*/
ipcMain.on('setBadge', async (e, ...args) => {
const dock = await import('./dock')
ipc.on('setBadge', (e, ...args) => {
const dock = require('./dock')
dock.setBadge(...args)
})
ipcMain.on('downloadFinished', async (e, ...args) => {
const dock = await import('./dock')
ipc.on('downloadFinished', (e, ...args) => {
const dock = require('./dock')
dock.downloadFinished(...args)
})
@@ -63,10 +70,10 @@ function init () {
* Player Events
*/
ipcMain.on('onPlayerOpen', async () => {
const powerSaveBlocker = await import('./power-save-blocker')
const shortcuts = await import('./shortcuts')
const thumbar = await import('./thumbar')
ipc.on('onPlayerOpen', function () {
const powerSaveBlocker = require('./power-save-blocker')
const shortcuts = require('./shortcuts')
const thumbar = require('./thumbar')
menu.togglePlaybackControls(true)
powerSaveBlocker.enable()
@@ -74,17 +81,17 @@ function init () {
thumbar.enable()
})
ipcMain.on('onPlayerUpdate', async (e, ...args) => {
const thumbar = await import('./thumbar')
ipc.on('onPlayerUpdate', function (e, ...args) {
const thumbar = require('./thumbar')
menu.onPlayerUpdate(...args)
thumbar.onPlayerUpdate(...args)
})
ipcMain.on('onPlayerClose', async () => {
const powerSaveBlocker = await import('./power-save-blocker')
const shortcuts = await import('./shortcuts')
const thumbar = await import('./thumbar')
ipc.on('onPlayerClose', function () {
const powerSaveBlocker = require('./power-save-blocker')
const shortcuts = require('./shortcuts')
const thumbar = require('./thumbar')
menu.togglePlaybackControls(false)
powerSaveBlocker.disable()
@@ -92,17 +99,17 @@ function init () {
thumbar.disable()
})
ipcMain.on('onPlayerPlay', async () => {
const powerSaveBlocker = await import('./power-save-blocker')
const thumbar = await import('./thumbar')
ipc.on('onPlayerPlay', function () {
const powerSaveBlocker = require('./power-save-blocker')
const thumbar = require('./thumbar')
powerSaveBlocker.enable()
thumbar.onPlayerPlay()
})
ipcMain.on('onPlayerPause', async () => {
const powerSaveBlocker = await import('./power-save-blocker')
const thumbar = await import('./thumbar')
ipc.on('onPlayerPause', function () {
const powerSaveBlocker = require('./power-save-blocker')
const thumbar = require('./thumbar')
powerSaveBlocker.disable()
thumbar.onPlayerPause()
@@ -112,7 +119,7 @@ function init () {
* Folder Watcher Events
*/
ipcMain.on('startFolderWatcher', () => {
ipc.on('startFolderWatcher', function () {
if (!modules.folderWatcher) {
log('IPC ERR: folderWatcher module is not defined.')
return
@@ -121,7 +128,7 @@ function init () {
modules.folderWatcher.start()
})
ipcMain.on('stopFolderWatcher', () => {
ipc.on('stopFolderWatcher', function () {
if (!modules.folderWatcher) {
log('IPC ERR: folderWatcher module is not defined.')
return
@@ -134,16 +141,16 @@ function init () {
* Shell
*/
ipcMain.on('openPath', async (e, ...args) => {
const shell = await import('./shell')
shell.openPath(...args)
ipc.on('openItem', (e, ...args) => {
const shell = require('./shell')
shell.openItem(...args)
})
ipcMain.on('showItemInFolder', async (e, ...args) => {
const shell = await import('./shell')
ipc.on('showItemInFolder', (e, ...args) => {
const shell = require('./shell')
shell.showItemInFolder(...args)
})
ipcMain.on('moveItemToTrash', async (e, ...args) => {
const shell = await import('./shell')
ipc.on('moveItemToTrash', (e, ...args) => {
const shell = require('./shell')
shell.moveItemToTrash(...args)
})
@@ -151,8 +158,8 @@ function init () {
* File handlers
*/
ipcMain.on('setDefaultFileHandler', async (e, flag) => {
const handlers = await import('./handlers')
ipc.on('setDefaultFileHandler', (e, flag) => {
const handlers = require('./handlers')
if (flag) handlers.install()
else handlers.uninstall()
@@ -162,8 +169,8 @@ function init () {
* Auto start on login
*/
ipcMain.on('setStartup', async (e, flag) => {
const startup = await import('./startup')
ipc.on('setStartup', (e, flag) => {
const startup = require('./startup')
if (flag) startup.install()
else startup.uninstall()
@@ -175,39 +182,37 @@ function init () {
const main = windows.main
ipcMain.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
ipcMain.on('setBounds', (e, ...args) => main.setBounds(...args))
ipcMain.on('setProgress', (e, ...args) => main.setProgress(...args))
ipcMain.on('setTitle', (e, ...args) => main.setTitle(...args))
ipcMain.on('show', () => main.show())
ipcMain.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
ipcMain.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
ipc.on('setProgress', (e, ...args) => main.setProgress(...args))
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
ipc.on('show', () => main.show())
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
/**
* External Media Player
*/
ipcMain.on('checkForExternalPlayer', async (e, path) => {
const externalPlayer = await import('./external-player')
ipc.on('checkForExternalPlayer', function (e, path) {
const externalPlayer = require('./external-player')
externalPlayer.checkInstall(path, err => {
externalPlayer.checkInstall(path, function (err) {
windows.main.send('checkForExternalPlayer', !err)
})
})
ipcMain.on('openExternalPlayer', async (e, ...args) => {
const externalPlayer = await import('./external-player')
const shortcuts = await import('./shortcuts')
const thumbar = await import('./thumbar')
ipc.on('openExternalPlayer', (e, ...args) => {
const externalPlayer = require('./external-player')
const thumbar = require('./thumbar')
menu.togglePlaybackControls(false)
shortcuts.disable()
thumbar.disable()
externalPlayer.spawn(...args)
})
ipcMain.on('quitExternalPlayer', async () => {
const externalPlayer = await import('./external-player')
ipc.on('quitExternalPlayer', () => {
const externalPlayer = require('./external-player')
externalPlayer.kill()
})
@@ -215,12 +220,11 @@ function init () {
* Message passing
*/
const oldEmit = ipcMain.emit
ipcMain.emit = (name, e, ...args) => {
const oldEmit = ipc.emit
ipc.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) {
console.dir(e.sender.getTitle())
if (e.sender.getTitle() === 'WebTorrent Hidden Window') {
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
// Send message to main window
windows.main.send(name, ...args)
log('webtorrent: got %s', name)
@@ -231,8 +235,8 @@ function init () {
} else {
// Queue message for webtorrent window, it hasn't finished loading yet
messageQueueMainToWebTorrent.push({
name,
args
name: name,
args: args
})
log('webtorrent: queueing %s', name)
}
@@ -240,13 +244,6 @@ function init () {
}
// 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
}

View File

@@ -1,7 +1,5 @@
import electron from '../../electron.cjs'
import * as windows from './windows/index.js'
const { app } = electron
module.exports = log
module.exports.error = error
/**
* 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
* 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) {
windows.main.send('log', ...args)
} else {
@@ -17,7 +21,7 @@ export default function log (...args) {
}
}
export function error (...args) {
function error (...args) {
if (app.ipcReady) {
windows.main.send('error', ...args)
} else {

View File

@@ -1,12 +1,23 @@
import electron from '../../electron.cjs'
import config from '../config.js'
import * as windows from './windows/index.js'
import shell from './shell.js'
module.exports = {
init,
togglePlaybackControls,
setWindowFocus,
setAllowNav,
onPlayerUpdate,
onToggleAlwaysOnTop,
onToggleFullScreen
}
const electron = require('electron')
const app = electron.app
const config = require('../config')
const windows = require('./windows')
let menu = null
function init () {
console.log('menu init')
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(menu)
}
@@ -59,11 +70,12 @@ function onToggleFullScreen (flag) {
}
function getMenuItem (label) {
for (const menuItem of menu.items) {
const submenuItem = menuItem.submenu.items.find(item => item.label === label)
if (submenuItem) return submenuItem
for (let i = 0; i < menu.items.length; i++) {
const menuItem = menu.items[i].submenu.items.find(function (item) {
return item.label === label
})
if (menuItem) return menuItem
}
return {}
}
function getMenuTemplate () {
@@ -76,24 +88,24 @@ function getMenuTemplate () {
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N',
click: async () => {
const dialog = await import('./dialog')
click: () => {
const dialog = require('./dialog')
dialog.openSeedDirectory()
}
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: async () => {
const dialog = await import('./dialog')
click: () => {
const dialog = require('./dialog')
dialog.openTorrentFile()
}
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: async () => {
const dialog = await import('./dialog')
click: () => {
const dialog = require('./dialog')
dialog.openTorrentAddress()
}
},
@@ -276,14 +288,6 @@ function getMenuTemplate () {
{
label: 'Resume All',
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,
click: () => {
const shell = require('./shell')
shell.openExternal(config.HOME_PAGE_URL)
}
},
{
label: 'Release Notes',
click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_RELEASES)
}
},
{
label: 'Contribute on GitHub',
click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL)
}
},
@@ -315,18 +322,21 @@ function getMenuTemplate () {
{
label: 'Report an Issue...',
click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_ISSUES)
}
},
{
label: 'Follow us on Twitter',
click: () => {
const shell = require('./shell')
shell.openExternal(config.TWITTER_PAGE_URL)
}
}
]
}
]
if (process.platform === 'darwin') {
// WebTorrent menu (Mac)
template.unshift({
@@ -347,7 +357,8 @@ function getMenuTemplate () {
type: 'separator'
},
{
role: 'services'
role: 'services',
submenu: []
},
{
type: 'separator'
@@ -411,8 +422,8 @@ function getMenuTemplate () {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: async () => {
const dialog = await import('./dialog')
click: () => {
const dialog = require('./dialog')
dialog.openSeedFile()
}
})
@@ -445,26 +456,9 @@ function getMenuTemplate () {
// File menu (Linux)
template[0].submenu.push({
label: 'Quit',
click: () => electron.app.quit()
click: () => app.quit()
})
}
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
}

View File

@@ -1,5 +1,10 @@
import { powerSaveBlocker } from 'electron'
import log from './log.js'
module.exports = {
enable,
disable
}
const electron = require('electron')
const log = require('./log')
let blockId = 0
@@ -7,23 +12,23 @@ let blockId = 0
* Block the system from entering low-power (sleep) mode or turning off the
* display.
*/
export function enable () {
if (powerSaveBlocker.isStarted(blockId)) {
function enable () {
if (electron.powerSaveBlocker.isStarted(blockId)) {
// If a power saver block already exists, do nothing.
return
}
blockId = powerSaveBlocker.start('prevent-display-sleep')
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
log(`powerSaveBlocker.enable: ${blockId}`)
}
/**
* Stop blocking the system from entering low-power mode.
*/
export function disable () {
if (!powerSaveBlocker.isStarted(blockId)) {
function disable () {
if (!electron.powerSaveBlocker.isStarted(blockId)) {
// If a power saver block does not exist, do nothing.
return
}
powerSaveBlocker.stop(blockId)
electron.powerSaveBlocker.stop(blockId)
log(`powerSaveBlocker.disable: ${blockId}`)
}

View File

@@ -1,22 +1,27 @@
import electron from '../../electron.cjs'
import log from './log.js'
module.exports = {
openExternal,
openItem,
showItemInFolder,
moveItemToTrash
}
const { shell } = electron
const electron = require('electron')
const log = require('./log')
/**
* Open the given external protocol URL in the desktops default manner.
*/
function openExternal (url) {
log(`openExternal: ${url}`)
shell.openExternal(url)
electron.shell.openExternal(url)
}
/**
* Open the given file in the desktops default manner.
*/
function openPath (path) {
log(`openPath: ${path}`)
shell.openPath(path)
function openItem (path) {
log(`openItem: ${path}`)
electron.shell.openItem(path)
}
/**
@@ -24,7 +29,7 @@ function openPath (path) {
*/
function showItemInFolder (path) {
log(`showItemInFolder: ${path}`)
shell.showItemInFolder(path)
electron.shell.showItemInFolder(path)
}
/**
@@ -32,12 +37,5 @@ function showItemInFolder (path) {
*/
function moveItemToTrash (path) {
log(`moveItemToTrash: ${path}`)
shell.trashItem(path)
}
export default {
openExternal,
openPath,
showItemInFolder,
moveItemToTrash
electron.shell.moveItemToTrash(path)
}

View File

@@ -1,25 +1,30 @@
import { globalShortcut } from 'electron'
import * as windows from './windows'
module.exports = {
disable,
enable
}
export function enable () {
const electron = require('electron')
const windows = require('./windows')
function enable () {
// Register play/pause media key, available on some keyboards.
globalShortcut.register(
electron.globalShortcut.register(
'MediaPlayPause',
() => windows.main.dispatch('playPause')
)
globalShortcut.register(
electron.globalShortcut.register(
'MediaNextTrack',
() => windows.main.dispatch('nextTrack')
)
globalShortcut.register(
electron.globalShortcut.register(
'MediaPreviousTrack',
() => windows.main.dispatch('previousTrack')
)
}
export function disable () {
function disable () {
// Return the media key to the OS, so other apps can use it.
globalShortcut.unregister('MediaPlayPause')
globalShortcut.unregister('MediaNextTrack')
globalShortcut.unregister('MediaPreviousTrack')
electron.globalShortcut.unregister('MediaPlayPause')
electron.globalShortcut.unregister('MediaNextTrack')
electron.globalShortcut.unregister('MediaPreviousTrack')
}

View File

@@ -1,33 +1,151 @@
import { app } from 'electron'
import path from 'path'
import { spawn } from 'child_process'
import handlers from './handlers.js'
module.exports = {
handleEvent
}
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 UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
const run = (args, done) => {
spawn(UPDATE_EXE, args, { detached: true }).on('close', done)
}
function handleEvent (cmd) {
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-install' || cmd === '--squirrel-updated') {
run([`--createShortcut=${EXE_NAME}`], app.quit)
if (cmd === '--squirrel-updated') {
// App was updated. (Called on new version of app)
updateShortcuts(function () {
app.quit()
})
return true
}
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
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
}
if (cmd === '--squirrel-obsolete') {
// App will be updated. (Called on outgoing version of app)
app.quit()
return true
}
if (cmd === '--squirrel-firstrun') {
// App is running for the first time. Do not quit, allow startup to continue.
return false
}
return false
}
/**
* Spawn a command and invoke the callback when it completes with an error and
* the output from standard out.
*/
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)
}

View File

@@ -1,12 +1,17 @@
import { APP_NAME } from '../config.js'
import AutoLaunch from 'auto-launch'
module.exports = {
install,
uninstall
}
const { APP_NAME } = require('../config')
const AutoLaunch = require('auto-launch')
const appLauncher = new AutoLaunch({
name: APP_NAME,
isHidden: true
})
export function install () {
function install () {
return appLauncher
.isEnabled()
.then(enabled => {
@@ -14,7 +19,7 @@ export function install () {
})
}
export function uninstall () {
function uninstall () {
return appLauncher
.isEnabled()
.then(enabled => {

View File

@@ -1,6 +1,21 @@
import path from 'path'
import config from '../config.js'
import * as windows from './windows'
module.exports = {
disable,
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 PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
@@ -17,7 +32,7 @@ let buttons = []
/**
* Show the Windows thumbnail toolbar buttons.
*/
export function enable () {
function enable () {
buttons = [
{
tooltip: 'Previous Track',
@@ -41,26 +56,26 @@ export function enable () {
/**
* Hide the Windows thumbnail toolbar buttons.
*/
export function disable () {
function disable () {
buttons = []
update()
}
export function onPlayerPause () {
function onPlayerPause () {
if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Play'
buttons[PLAY_PAUSE].icon = PLAY_ICON
update()
}
export function onPlayerPlay () {
function onPlayerPlay () {
if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Pause'
buttons[PLAY_PAUSE].icon = PAUSE_ICON
update()
}
export function onPlayerUpdate (state) {
function onPlayerUpdate (state) {
if (!isEnabled()) return
buttons[PREV].flags = [state.hasPrevious ? 'enabled' : 'disabled']
buttons[NEXT].flags = [state.hasNext ? 'enabled' : 'disabled']

View File

@@ -1,10 +1,19 @@
import { app, Tray, Menu } from 'electron'
import config from '../config.js'
import * as windows from './windows'
module.exports = {
hasTray,
init,
setWindowFocus
}
const electron = require('electron')
const app = electron.app
const config = require('../config')
const windows = require('./windows')
let tray
export function init () {
function init () {
if (process.platform === 'linux') {
initLinux()
}
@@ -17,17 +26,17 @@ export function init () {
/**
* Returns true if there a tray icon is active.
*/
export function hasTray () {
function hasTray () {
return !!tray
}
export function setWindowFocus (flag) {
function setWindowFocus (flag) {
if (!tray) return
updateTrayMenu()
}
function initLinux () {
checkLinuxTraySupport(err => {
checkLinuxTraySupport(function (err) {
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) {
const cp = await import('child_process')
function checkLinuxTraySupport (cb) {
const cp = require('child_process')
// Check that libappindicator libraries are installed in system.
cp.exec('ldconfig -p | grep libappindicator', (err, stdout) => {
// Check that we're on Ubuntu (or another debian system) and that we have
// libappindicator1. If WebTorrent was installed from the deb file, we should
// always have it. If it was installed from the zip file, we might not.
cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) {
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 () {
tray = new Tray(getIconPath())
tray = new electron.Tray(getIconPath())
// On Windows, left click opens the app, right click opens the context menu.
// On Linux, any click (left or right) opens the context menu.
@@ -61,7 +78,7 @@ function createTray () {
}
function updateTrayMenu () {
const contextMenu = Menu.buildFromTemplate(getMenuTemplate())
const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
tray.setContextMenu(contextMenu)
}

View File

@@ -1,10 +1,13 @@
import electron from '../../electron.cjs'
import get from 'simple-get'
import config from '../config.js'
import log from './log.js'
import * as windows from './windows/index.js'
module.exports = {
init
}
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 +
'?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
// updates and show the user a modal notification.
function initLinux () {
@@ -46,31 +47,31 @@ function onResponse (err, res, data) {
}
function initDarwinWin32 () {
autoUpdater.on(
electron.autoUpdater.on(
'error',
(err) => log(`Update error: ${err.message}`)
(err) => log.error(`Update error: ${err.message}`)
)
autoUpdater.on(
electron.autoUpdater.on(
'checking-for-update',
() => log('Checking for update')
)
autoUpdater.on(
electron.autoUpdater.on(
'update-available',
() => log('Update available')
)
autoUpdater.on(
electron.autoUpdater.on(
'update-not-available',
() => log('No update available')
)
autoUpdater.on(
electron.autoUpdater.on(
'update-downloaded',
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
)
autoUpdater.setFeedURL({ url: AUTO_UPDATE_URL })
autoUpdater.checkForUpdates()
electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
electron.autoUpdater.checkForUpdates()
}

View File

@@ -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)
*/
export function init () {
function init () {
if (process.platform !== 'win32') return
app.setUserTasks(getUserTasks())
}

View File

@@ -1,12 +1,11 @@
import config from '../../config.js'
import electron from '../../../electron.cjs'
import * as RemoteMain from '@electron/remote/main/index.js'
export const about = {
const about = module.exports = {
init,
win: null
}
const config = require('../../config')
const electron = require('electron')
function init () {
if (about.win) {
return about.win.show()
@@ -16,7 +15,7 @@ function init () {
backgroundColor: '#ECECEC',
center: true,
fullscreen: false,
height: 250,
height: 170,
icon: getIconPath(),
maximizable: false,
minimizable: false,
@@ -26,26 +25,21 @@ function init () {
title: 'About ' + config.APP_WINDOW_TITLE,
useContentSize: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true,
backgroundThrottling: false
nodeIntegration: true
},
width: 300
})
RemoteMain.enable(win.webContents)
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()
// 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
})
}

View File

@@ -1,3 +1,3 @@
export * from './about.js'
export * from './main.js'
export * from './webtorrent.js'
exports.about = require('./about')
exports.main = require('./main')
exports.webtorrent = require('./webtorrent')

View File

@@ -1,13 +1,4 @@
import debounce from 'debounce'
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 = {
const main = module.exports = {
dispatch,
hide,
init,
@@ -23,6 +14,15 @@ export const main = {
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) {
if (main.win) {
return main.win.show()
@@ -30,8 +30,9 @@ function init (state, options) {
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',
backgroundThrottling: false, // do not throttle animations/timers when page is background
darkTheme: true, // Forces dark theme (GTK+3)
height: initialBounds.height,
icon: getIconPath(), // Window icon (Windows, Linux)
@@ -43,17 +44,12 @@ function init (state, options) {
useContentSize: true, // Specify web page size without OS chrome
width: initialBounds.width,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true,
backgroundThrottling: false
nodeIntegration: true
},
x: initialBounds.x,
y: initialBounds.y
})
RemoteMain.enable(win.webContents)
console.log(config.WINDOW_MAIN)
win.loadURL(config.WINDOW_MAIN)
win.once('ready-to-show', () => {
@@ -68,7 +64,7 @@ function init (state, options) {
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
// before our drag-and-drop handlers have been initialized.
e.preventDefault()
@@ -100,10 +96,10 @@ function init (state, options) {
send('windowBoundsChanged', e.sender.getBounds())
}, 1000))
win.on('close', async e => {
win.on('close', e => {
if (process.platform !== 'darwin') {
const { hasTray } = await import('../tray.js')
if (!hasTray()) {
const tray = require('../tray')
if (!tray.hasTray()) {
app.quit()
return
}
@@ -154,7 +150,7 @@ function setBounds (bounds, maximize) {
log('setBounds: maximizing')
main.win.maximize()
} else if (maximize === false && main.win.isMaximized()) {
log('setBounds: minimizing')
log('setBounds: unmaximizing')
main.win.unmaximize()
}
@@ -164,7 +160,7 @@ function setBounds (bounds, maximize) {
log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`)
if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen
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.y = Math.round(scr.bounds.y + (scr.bounds.height / 2) - (bounds.height / 2))
log(`setBounds: centered to ${JSON.stringify(bounds)}`)

View File

@@ -1,8 +1,4 @@
import electron from '../../../electron.cjs'
import config from '../../config.js'
import * as RemoteMain from '@electron/remote/main/index.js'
export const webtorrent = {
const webtorrent = module.exports = {
init,
send,
show,
@@ -10,9 +6,14 @@ export const webtorrent = {
win: null
}
const electron = require('electron')
const config = require('../../config')
function init () {
const win = webtorrent.win = new electron.BrowserWindow({
backgroundColor: '#1E1E1E',
backgroundThrottling: false, // do not throttle animations/timers when page is background
center: true,
fullscreen: false,
fullscreenable: false,
@@ -25,18 +26,15 @@ function init () {
title: 'webtorrent-hidden-window',
useContentSize: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true,
backgroundThrottling: false
nodeIntegration: true
},
width: 150
})
RemoteMain.enable(win.webContents)
win.loadURL(config.WINDOW_WEBTORRENT)
// Prevent killing the WebTorrent process
win.on('close', e => {
win.on('close', function (e) {
if (electron.app.isQuitting) {
return
}

View File

@@ -1,7 +1,8 @@
import * as React from 'react'
import { dispatcher } from '../lib/dispatcher.js'
const React = require('react')
export default class CreateTorrentErrorPage extends React.Component {
const { dispatcher } = require('../lib/dispatcher')
module.exports = class CreateTorrentErrorPage extends React.Component {
render () {
return (
<div className='create-torrent'>

View File

@@ -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')
}
}
}

View File

@@ -1,7 +1,8 @@
import * as React from 'react'
import { dispatcher } from '../lib/dispatcher.js'
const React = require('react')
export default class Header extends React.Component {
const { dispatcher } = require('../lib/dispatcher')
class Header extends React.Component {
render () {
const loc = this.props.state.location
return (
@@ -55,3 +56,5 @@ export default class Header extends React.Component {
)
}
}
module.exports = Header

View File

@@ -1,8 +1,9 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import * as colors from 'material-ui/styles/colors'
const React = require('react')
const PropTypes = require('prop-types')
export default class Heading extends React.Component {
const colors = require('material-ui/styles/colors')
class Heading extends React.Component {
static get propTypes () {
return {
level: PropTypes.number
@@ -30,3 +31,5 @@ export default class Heading extends React.Component {
)
}
}
module.exports = Heading

View File

@@ -1,10 +1,8 @@
import * as React from 'react'
import flatButton from 'material-ui/FlatButton'
import raisedButton from 'material-ui/RaisedButton'
const React = require('react')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
const FlatButton = flatButton.default
const RaisedButton = raisedButton.default
export default class ModalOKCancel extends React.Component {
module.exports = class ModalOKCancel extends React.Component {
render () {
const cancelStyle = { marginRight: 10, color: 'black' }
const { cancelText, onCancel, okText, onOK } = this.props

View File

@@ -1,14 +1,12 @@
import * as React from 'react'
import textField from 'material-ui/TextField'
import electron from 'electron'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch, dispatcher } from '../lib/dispatcher.js'
import { isMagnetLink } from '../lib/torrent-player.js'
const React = require('react')
const TextField = require('material-ui/TextField').default
const { clipboard } = require('electron')
const TextField = textField.default
const { clipboard } = electron
const ModalOKCancel = require('./modal-ok-cancel')
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 () {
return (
<div className='open-torrent-address-modal'>

View File

@@ -1,17 +1,19 @@
import path from 'path'
import * as colors from 'material-ui/styles/colors'
import remote from '@electron/remote'
import * as React from 'react'
import PropTypes from 'prop-types'
import raisedButton from 'material-ui/RaisedButton'
import textField from 'material-ui/TextField'
const path = require('path')
const colors = require('material-ui/styles/colors')
const electron = require('electron')
const React = require('react')
const PropTypes = require('prop-types')
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.
// Uses the system Open File dialog.
// You can't edit the text field directly.
export default class PathSelector extends React.Component {
class PathSelector extends React.Component {
static propTypes () {
return {
className: PropTypes.string,
@@ -34,9 +36,14 @@ export default class PathSelector extends React.Component {
properties: ['openFile', 'openDirectory']
}, this.props.dialog)
const filenames = remote.dialog.showOpenDialogSync(remote.getCurrentWindow(), opts)
if (!Array.isArray(filenames)) return
this.props.onChange && this.props.onChange(filenames[0])
remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
opts,
(filenames) => {
if (!Array.isArray(filenames)) return
this.props.onChange && this.props.onChange(filenames[0])
}
)
}
render () {
@@ -81,3 +88,5 @@ export default class PathSelector extends React.Component {
)
}
}
module.exports = PathSelector

View File

@@ -1,8 +1,9 @@
import * as React from 'react'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch, dispatcher } from '../lib/dispatcher.js'
const React = require('react')
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 () {
const state = this.props.state
const message = state.modal.deleteData

View File

@@ -1,10 +1,9 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import raisedButton from 'material-ui/RaisedButton'
const React = require('react')
const PropTypes = require('prop-types')
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 () {
return {
defaultExpanded: PropTypes.bool,
@@ -22,9 +21,11 @@ export default class ShowMore extends React.Component {
constructor (props) {
super(props)
this.state = {
expanded: !!this.props.defaultExpanded
}
this.handleClick = this.handleClick.bind(this)
}
@@ -50,3 +51,5 @@ export default class ShowMore extends React.Component {
)
}
}
module.exports = ShowMore

View File

@@ -1,9 +1,10 @@
import * as React from 'react'
import { shell } from 'electron'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatcher } from '../lib/dispatcher.js'
const React = require('react')
const electron = require('electron')
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 () {
const state = this.props.state
const err = state.modal.error
@@ -35,7 +36,7 @@ export default class UnsupportedMediaModal extends React.Component {
}
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
const state = this.props.state

View File

@@ -1,9 +1,10 @@
import * as React from 'react'
import { shell } from 'electron'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch } from '../lib/dispatcher.js'
const React = require('react')
const electron = require('electron')
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 () {
const state = this.props.state
return (
@@ -24,7 +25,7 @@ export default class UpdateAvailableModal extends React.Component {
function handleShow () {
// 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')
}

View File

@@ -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
}
}

View File

@@ -1,6 +1,6 @@
import { ipcRenderer } from 'electron'
const { ipcRenderer } = require('electron')
export default class FolderWatcherController {
module.exports = class FolderWatcherController {
start () {
console.log('-- IPC: start folder watcher')
ipcRenderer.send('startFolderWatcher')

View File

@@ -1,8 +1,10 @@
import { ipcRenderer } from 'electron'
import telemetry from '../lib/telemetry.js'
import Playlist from '../lib/playlist.js'
const { ipcRenderer } = require('electron')
const telemetry = require('../lib/telemetry')
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) {
this.state = state
}
@@ -21,10 +23,10 @@ export default class MediaController {
telemetry.logPlayAttempt('error')
state.playing.location = 'error'
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
ipcRenderer.once('checkForExternalPlayer', (e, isInstalled) => {
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
state.modal = {
id: 'unsupported-media-modal',
error,
error: error,
externalPlayerInstalled: isInstalled
}
})
@@ -54,7 +56,7 @@ export default class MediaController {
const state = this.state
state.playing.location = 'external'
const onServerRunning = () => {
const onServerRunning = function () {
state.playing.isReady = true
telemetry.logPlayAttempt('external')

View File

@@ -1,29 +1,21 @@
import { ipcRenderer } from 'electron'
import path from 'path'
import Cast from '../lib/cast.js'
import { dispatch } from '../lib/dispatcher.js'
import telemetry from '../lib/telemetry.js'
import { UnplayableFileError, UnplayableTorrentError } from '../lib/errors.js'
import sound from '../lib/sound.js'
import TorrentPlayer from '../lib/torrent-player.js'
import TorrentSummary from '../lib/torrent-summary.js'
import Playlist from '../lib/playlist.js'
import State from '../lib/state.js'
// 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)
}
}
export default class PlaybackController {
const electron = require('electron')
const path = require('path')
const Cast = require('../lib/cast')
const { dispatch } = require('../lib/dispatcher')
const telemetry = require('../lib/telemetry')
const { UnplayableFileError, UnplayableTorrentError } = require('../lib/errors')
const sound = require('../lib/sound')
const TorrentPlayer = require('../lib/torrent-player')
const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist')
const State = require('../lib/state')
const ipcRenderer = electron.ipcRenderer
// Controls playback of torrents and files within torrents
// both local (<video>,<audio>,external player) and remote (cast)
module.exports = class PlaybackController {
constructor (state, config, update) {
this.state = state
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
playFile (infoHash, index /* optional */) {
this.pauseActiveTorrents(infoHash)
const state = this.state
if (state.location.url() === 'player') {
this.updatePlayer(infoHash, index, false, (err) => {
if (err) { dispatch('error', err) } else { this.play() }
if (err) dispatch('error', err)
else this.play()
})
} else {
let initialized = false
@@ -47,10 +41,13 @@ export default class PlaybackController {
url: 'player',
setup: (cb) => {
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
if (index === undefined) return cb(new UnplayableTorrentError())
initialized = true
this.openPlayer(infoHash, index, (err) => {
if (!err) this.play()
cb(err)
@@ -58,37 +55,45 @@ export default class PlaybackController {
},
destroy: () => this.closePlayer()
}, (err) => {
if (err) { dispatch('error', err) }
if (err) dispatch('error', err)
})
}
}
// Open a file in OS default app.
openPath (infoHash, index) {
openItem (infoHash, index) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const filePath = path.join(torrentSummary.path, torrentSummary.files[index].path)
ipcRenderer.send('openPath', filePath)
const filePath = path.join(
torrentSummary.path,
torrentSummary.files[index].path)
ipcRenderer.send('openItem', filePath)
}
// Toggle (play or pause) the currently playing media
playPause () {
const state = this.state
if (state.location.url() !== 'player') return
// force rerendering if window is hidden,
// in order to bypass `raf` and play/pause media immediately
const mediaTag = document.querySelector('video,audio')
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) {
// Playback Priority: pause all active torrents if needed.
if (!this.state.saved.prefs.highestPlaybackPriority) return
// Do not pause active torrents if playing a fully downloaded torrent.
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
if (torrentSummary.status === 'seeding') return
dispatch('prioritizeTorrent', infoHash)
}
@@ -96,9 +101,11 @@ export default class PlaybackController {
nextTrack () {
const state = this.state
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
this.updatePlayer(state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
if (err) { dispatch('error', err) } else { this.play() }
})
this.updatePlayer(
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 () {
const state = this.state
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
this.updatePlayer(state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
if (err) { dispatch('error', err) } else { this.play() }
})
this.updatePlayer(
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)
return console.trace()
}
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
if (isCasting(this.state)) Cast.seek(time)
else this.state.playing.jumpToTime = time
}
// Change playback speed. 1 = faster, -1 = slower
@@ -197,6 +193,7 @@ export default class PlaybackController {
setVolume (volume) {
// check if its in [0.0 - 1.0] range
volume = Math.max(0, Math.min(1, volume))
const state = this.state
if (isCasting(state)) {
Cast.setVolume(volume)
@@ -213,6 +210,7 @@ export default class PlaybackController {
showOrHidePlayerControls () {
const state = this.state
const hideControls = state.shouldHidePlayerControls()
if (hideControls !== state.playing.hideControls) {
state.playing.hideControls = hideControls
return true
@@ -224,10 +222,13 @@ export default class PlaybackController {
openPlayer (infoHash, index, cb) {
const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
state.playing.infoHash = torrentSummary.infoHash
state.playing.isReady = false
// update UI to show pending playback
sound.play('PLAY')
this.startServer(torrentSummary)
ipcRenderer.send('onPlayerOpen')
this.updatePlayer(infoHash, index, true, cb)
@@ -236,12 +237,15 @@ export default class PlaybackController {
// Starts WebTorrent server for media streaming
startServer (torrentSummary) {
const state = this.state
if (torrentSummary.status === 'paused') {
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, () => onTorrentReady())
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
() => onTorrentReady())
} else {
onTorrentReady()
}
function onTorrentReady () {
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
ipcRenderer.once('wt-server-running', () => { state.playing.isReady = true })
@@ -251,23 +255,26 @@ export default class PlaybackController {
// Called each time the current file changes
updatePlayer (infoHash, index, resume, cb) {
const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
const fileSummary = torrentSummary.files[index]
if (!TorrentPlayer.isPlayable(fileSummary)) {
torrentSummary.mostRecentFileIndex = undefined
return cb(new UnplayableFileError())
}
torrentSummary.mostRecentFileIndex = index
// update state
state.playing.infoHash = infoHash
state.playing.fileIndex = index
state.playing.fileName = fileSummary.name
state.playing.type = TorrentPlayer.isVideo(fileSummary)
? 'video'
: TorrentPlayer.isAudio(fileSummary)
? 'audio'
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
: 'other'
// pick up where we left off
// pick up where we left off
let jumpToTime = 0
if (resume && fileSummary.currentTime) {
const fraction = fileSummary.currentTime / fileSummary.duration
@@ -277,24 +284,30 @@ export default class PlaybackController {
}
}
state.playing.jumpToTime = jumpToTime
// if it's audio, parse out the metadata (artist, title, etc)
if (torrentSummary.status === 'paused') {
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata)
} else {
getAudioMetadata()
}
function getAudioMetadata () {
if (state.playing.type === 'audio') {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
}
}
// if it's video, check for subtitles files that are done downloading
dispatch('checkForSubtitles')
// enable previously selected subtitle track
if (fileSummary.selectedSubtitle) {
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
}
state.window.title = fileSummary.name
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer')
@@ -302,8 +315,10 @@ export default class PlaybackController {
cb()
return
}
// otherwise, play the video
this.update()
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
cb()
}
@@ -334,13 +349,33 @@ export default class PlaybackController {
dispatch('toggleFullScreen', false)
}
restoreBounds(state)
// Tell the WebTorrent process to kill the torrent-to-HTTP server
ipcRenderer.send('wt-stop-server')
ipcRenderer.send('onPlayerClose')
// Playback Priority: resume previously paused downloads.
if (this.state.saved.prefs.highestPlaybackPriority) {
dispatch('resumePausedTorrents')
}
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)
}
}

View File

@@ -1,7 +1,8 @@
import { dispatch } from '../lib/dispatcher.js'
import { ipcRenderer } from 'electron'
const { dispatch } = require('../lib/dispatcher')
const ipcRenderer = require('electron').ipcRenderer
export default class PrefsController {
// Controls the Preferences screen
module.exports = class PrefsController {
constructor (state, config) {
this.state = state
this.config = config
@@ -12,7 +13,7 @@ export default class PrefsController {
const state = this.state
state.location.go({
url: 'preferences',
setup (cb) {
setup: function (cb) {
// initialize preferences
state.window.title = 'Preferences'
ipcRenderer.send('setAllowNav', false)

View File

@@ -1,22 +1,26 @@
import remote from '@electron/remote'
import fs from 'fs'
import path from 'path'
import parallel from 'run-parallel'
import { dispatch } from '../lib/dispatcher.js'
const electron = require('electron')
const fs = require('fs')
const path = require('path')
const parallel = require('run-parallel')
export default class SubtitlesController {
const remote = electron.remote
const { dispatch } = require('../lib/dispatcher')
module.exports = class SubtitlesController {
constructor (state) {
this.state = state
}
openSubtitles () {
const filenames = remote.dialog.showOpenDialogSync({
remote.dialog.showOpenDialog({
title: 'Select a subtitles file.',
filters: [{ name: 'Subtitles', extensions: ['vtt', 'srt'] }],
properties: ['openFile']
}, (filenames) => {
if (!Array.isArray(filenames)) return
this.addSubtitles(filenames, true)
})
if (!Array.isArray(filenames)) return
this.addSubtitles(filenames, true)
}
selectSubtitle (ix) {
@@ -36,11 +40,12 @@ export default class SubtitlesController {
// Read the files concurrently, then add all resulting subtitle tracks
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(tasks, (err, tracks) => {
parallel(tasks, function (err, tracks) {
if (err) return dispatch('error', err)
// No dupes allowed
tracks.forEach((track, i) => {
for (let i = 0; i < tracks.length; i++) {
// No dupes allowed
const track = tracks[i]
let trackIndex = subtitles.tracks.findIndex((t) =>
track.filePath === t.filePath)
@@ -53,7 +58,7 @@ export default class SubtitlesController {
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
subtitles.selectedIndex = trackIndex
}
})
}
// Finally, make sure no two tracks have the same label
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
const concat = await import('simple-concat')
const LanguageDetect = await import('languagedetect')
const srtToVtt = await import('srt-to-vtt')
const concat = require('simple-concat')
const LanguageDetect = require('languagedetect')
const srtToVtt = require('srt-to-vtt')
// Read the .SRT or .VTT file, parse it, add subtitle track
const filePath = file.path || file
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.')
// Detect what language the subtitles are in
@@ -105,7 +110,7 @@ async function loadSubtitle (file, cb) {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected,
label: langDetected,
filePath
filePath: filePath
}
cb(null, track)
@@ -114,8 +119,8 @@ async function loadSubtitle (file, cb) {
// Checks whether a language name like 'English' or 'German' matches the system
// language, aka the current locale
async function isSystemLanguage (language) {
const iso639 = await import('iso-639-1')
function isSystemLanguage (language) {
const iso639 = require('iso-639-1')
const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
const langIso = iso639.getCode(language) // eg 'de' if language is 'German'
return langIso === osLangISO
@@ -125,7 +130,7 @@ async function isSystemLanguage (language) {
// Labels each track by language, eg 'German', 'English', 'English 2', ...
function relabelSubtitles (subtitles) {
const counts = {}
subtitles.tracks.forEach(track => {
subtitles.tracks.forEach(function (track) {
const lang = track.language
counts[lang] = (counts[lang] || 0) + 1
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang

View File

@@ -1,10 +1,11 @@
import path from 'path'
import { ipcRenderer } from 'electron'
import TorrentSummary from '../lib/torrent-summary.js'
import sound from '../lib/sound.js'
import { dispatch } from '../lib/dispatcher.js'
const path = require('path')
const ipcRenderer = require('electron').ipcRenderer
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) {
this.state = state
}
@@ -24,7 +25,7 @@ export default class TorrentController {
}
torrentSummary = {
torrentKey,
torrentKey: torrentKey,
status: 'new'
}
torrents.unshift(torrentSummary)
@@ -176,7 +177,7 @@ function showDoneNotification (torrent) {
silent: true
})
notif.onclick = () => {
notif.onclick = function () {
ipcRenderer.send('show')
}

View File

@@ -1,18 +1,18 @@
import fs from 'fs'
import path from 'path'
import electron from 'electron'
import remote from '@electron/remote'
const fs = require('fs')
const path = require('path')
const electron = require('electron')
import { dispatch } from '../lib/dispatcher.js'
import { TorrentKeyNotFoundError } from '../lib/errors.js'
import sound from '../lib/sound.js'
import TorrentSummary from '../lib/torrent-summary.js'
const { dispatch } = require('../lib/dispatcher')
const { TorrentKeyNotFoundError } = require('../lib/errors')
const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary')
const { ipcRenderer, clipboard } = electron
const ipcRenderer = electron.ipcRenderer
const instantIoRegex = /^(https:\/\/)?instant\.io\/#/
export default class TorrentListController {
// Controls the torrent list: creating, adding, deleting, & manipulating torrents
module.exports = class TorrentListController {
constructor (state) {
this.state = state
}
@@ -55,7 +55,7 @@ export default class TorrentListController {
if (files.length === 0 || typeof files[0] !== 'string') {
this.state.location.go({
url: 'create-torrent',
files,
files: files,
setup: (cb) => {
this.state.window.title = 'Create New Torrent'
cb(null)
@@ -95,7 +95,7 @@ export default class TorrentListController {
if (!fileOrFolder) return start()
// Existing torrent: check that the path is still there
fs.stat(fileOrFolder, err => {
fs.stat(fileOrFolder, function (err) {
if (err) {
s.error = 'path-missing'
dispatch('backToList')
@@ -157,8 +157,10 @@ export default class TorrentListController {
prioritizeTorrent (infoHash) {
this.state.saved.torrents
.filter(torrent => ['downloading', 'seeding'].includes(torrent.status)) // Active torrents only.
.forEach((torrent) => { // Pause all active torrents except the one that started playing.
.filter((torrent) => { // We're interested in active torrents only.
return (['downloading', 'seeding'].indexOf(torrent.status) !== -1)
})
.map((torrent) => { // Pause all active torrents except the one that started playing.
if (infoHash === torrent.infoHash) return
// Pause torrent without playing sounds.
@@ -173,7 +175,7 @@ export default class TorrentListController {
resumePausedTorrents () {
console.log('Playback Priority: resuming paused torrents')
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)
})
@@ -199,43 +201,26 @@ export default class TorrentListController {
}
}
confirmDeleteAllTorrents (deleteData) {
this.state.modal = {
id: 'delete-all-torrents-modal',
deleteData
}
}
// TODO: use torrentKey, not infoHash
deleteTorrent (infoHash, deleteData) {
ipcRenderer.send('wt-stop-torrenting', infoHash)
const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) {
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
this.state.saved.torrents.splice(index, 1)
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
this.state.location.clearForward('player')
@@ -252,88 +237,77 @@ export default class TorrentListController {
openTorrentContextMenu (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',
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
}))
menu.append(new remote.MenuItem({
menu.append(new electron.remote.MenuItem({
label: 'Remove Data File',
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
}))
menu.append(new remote.MenuItem({
menu.append(new electron.remote.MenuItem({
type: 'separator'
}))
if (torrentSummary.files) {
menu.append(new remote.MenuItem({
menu.append(new electron.remote.MenuItem({
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
click: () => showItemInFolder(torrentSummary)
}))
menu.append(new remote.MenuItem({
menu.append(new electron.remote.MenuItem({
type: 'separator'
}))
}
menu.append(new remote.MenuItem({
menu.append(new electron.remote.MenuItem({
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',
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...',
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey),
enabled: torrentSummary.torrentFileName != null
}))
menu.append(new remote.MenuItem({
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() })
menu.popup(electron.remote.getCurrentWindow())
}
// Takes a torrentSummary or torrentKey
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
saveTorrentFileAs (torrentKey) {
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
if (!torrentSummary) throw new TorrentKeyNotFoundError(torrentKey)
if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
const downloadPath = this.state.saved.prefs.downloadPath
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
const win = remote.getCurrentWindow()
const win = electron.remote.getCurrentWindow()
const opts = {
title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
],
buttonLabel: 'Save'
]
}
const savePath = remote.dialog.showSaveDialogSync(win, opts)
if (!savePath) return // They clicked Cancel
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, (err, torrentFile) => {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, err => {
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
if (!savePath) return // They clicked Cancel
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return dispatch('error', err)
})
})
})
}
@@ -345,11 +319,11 @@ function findFilesRecursive (paths, cb_) {
if (paths.length > 1) {
let numComplete = 0
const ret = []
paths.forEach(path => {
findFilesRecursive([path], fileObjs => {
paths.forEach(function (path) {
findFilesRecursive([path], function (fileObjs) {
ret.push(...fileObjs)
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)
}
})
@@ -358,7 +332,7 @@ function findFilesRecursive (paths, cb_) {
}
const fileOrFolder = paths[0]
fs.stat(fileOrFolder, (err, stat) => {
fs.stat(fileOrFolder, function (err, stat) {
if (err) return dispatch('error', err)
// Files: return name, path, and size
@@ -373,7 +347,7 @@ function findFilesRecursive (paths, cb_) {
// Folders: recurse, make a list of all the files
const folderPath = fileOrFolder
fs.readdir(folderPath, (err, fileNames) => {
fs.readdir(folderPath, function (err, fileNames) {
if (err) return dispatch('error', err)
const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
findFilesRecursive(paths, cb_)
@@ -383,7 +357,7 @@ function findFilesRecursive (paths, cb_) {
function deleteFile (path) {
if (!path) return
fs.unlink(path, err => {
fs.unlink(path, function (err) {
if (err) dispatch('error', err)
})
}
@@ -397,14 +371,3 @@ function moveItemToTrash (torrentSummary) {
function showItemInFolder (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)
}

View File

@@ -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) {
this.state = state
}
@@ -12,7 +13,7 @@ export default class UpdateController {
console.log('new version skipped by user: v' + version)
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

View File

@@ -1,18 +1,40 @@
import http from 'http'
import config from '../src/config.js'
import { CastingError } from './errors.js'
// The Cast module talks to Airplay and Chromecast
// * Modifies state when things change
// * 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
let airplayer, chromecasts, dlnacasts
// App state. Cast modifies state.playing and state.errors in response to events
let state
// Callback to notify module users when state has changed
let update
// setInterval() for updating cast status
let statusInterval = null
// Start looking for cast devices on the local network
function init (appState, callback) {
state = appState
update = callback
// Don't actually cast during integration tests
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
if (config.IS_TEST) {
@@ -21,26 +43,32 @@ function init (appState, callback) {
state.devices.dlna = testPlayer('dlna')
return
}
// Load modules, scan the network for devices
airplayer = require('airplayer')()
chromecasts = require('chromecasts')()
dlnacasts = require('dlnacasts')()
state.devices.chromecast = chromecastPlayer()
state.devices.dlna = dlnaPlayer()
state.devices.airplay = airplayPlayer()
// 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?
// From looking at the code, chromecasts.players only grows, never shrinks
state.devices.chromecast.addDevice(device)
})
dlnacasts.on('update', device => {
dlnacasts.on('update', function (device) {
state.devices.dlna.addDevice(device)
})
airplayer.on('update', device => {
airplayer.on('update', function (device) {
state.devices.airplay.addDevice(device)
})
}
// integration test player implementation
function testPlayer (type) {
return {
@@ -53,17 +81,20 @@ function testPlayer (type) {
seek,
volume
}
function getDevices () {
return [{ name: type + '-1' }, { name: type + '-2' }]
}
function open () { }
function play () { }
function pause () { }
function stop () { }
function status () { }
function seek () { }
function volume () { }
function open () {}
function play () {}
function pause () {}
function stop () {}
function status () {}
function seek () {}
function volume () {}
}
// chromecast player implementation
function chromecastPlayer () {
const ret = {
@@ -79,11 +110,13 @@ function chromecastPlayer () {
volume
}
return ret
function getDevices () {
return chromecasts.players
}
function addDevice (device) {
device.on('error', err => {
device.on('error', function (err) {
if (device !== ret.device) return
state.playing.location = 'local'
state.errors.push({
@@ -92,12 +125,13 @@ function chromecastPlayer () {
})
update()
})
device.on('disconnect', () => {
device.on('disconnect', function () {
if (device !== ret.device) return
state.playing.location = 'local'
update()
})
}
function serveSubtitles (callback) {
const subtitles = state.playing.subtitles
const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex]
@@ -111,22 +145,23 @@ function chromecastPlayer () {
'Transfer-Encoding': 'chunked'
})
res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64'))
}).listen(0, () => {
}).listen(0, function () {
const port = ret.subServer.address().port
const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/'
callback(subtitlesUrl)
})
}
}
function open () {
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, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name,
subtitles: subtitlesUrl ? [subtitlesUrl] : [],
autoSubtitles: !!subtitlesUrl
}, err => {
}, function (err) {
if (err) {
state.playing.location = 'local'
state.errors.push({
@@ -140,28 +175,35 @@ function chromecastPlayer () {
})
})
}
function play (callback) {
ret.device.play(null, null, callback)
}
function pause (callback) {
ret.device.pause(callback)
}
function stop (callback) {
ret.device.stop(callback)
if (ret.subServer) {
ret.subServer.close()
}
}
function status () {
ret.device.status(handleStatus)
}
function seek (time, callback) {
ret.device.seek(time, callback)
}
function volume (volume, callback) {
ret.device.volume(volume, callback)
}
}
// airplay player implementation
function airplayPlayer () {
const ret = {
@@ -177,8 +219,9 @@ function airplayPlayer () {
volume
}
return ret
function addDevice (player) {
player.on('event', event => {
player.on('event', function (event) {
switch (event.state) {
case 'loading':
break
@@ -194,11 +237,13 @@ function airplayPlayer () {
update()
})
}
function getDevices () {
return airplayer.players
}
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) {
state.playing.location = 'local'
state.errors.push({
@@ -211,17 +256,21 @@ function airplayPlayer () {
update()
})
}
function play (callback) {
ret.device.resume(callback)
}
function pause (callback) {
ret.device.pause(callback)
}
function stop (callback) {
ret.device.stop(callback)
}
function status () {
ret.device.playbackInfo((err, res, status) => {
ret.device.playbackInfo(function (err, res, status) {
if (err) {
state.playing.location = 'local'
state.errors.push({
@@ -235,15 +284,18 @@ function airplayPlayer () {
}
})
}
function seek (time, callback) {
ret.device.scrub(time, callback)
}
function volume (volume, callback) {
// AirPlay doesn't support volume
// TODO: We should just disable the volume slider
state.playing.volume = volume
}
}
// DLNA player implementation
function dlnaPlayer (player) {
const ret = {
@@ -259,11 +311,13 @@ function dlnaPlayer (player) {
volume
}
return ret
function getDevices () {
return dlnacasts.players
}
function addDevice (device) {
device.on('error', err => {
device.on('error', function (err) {
if (device !== ret.device) return
state.playing.location = 'local'
state.errors.push({
@@ -272,19 +326,20 @@ function dlnaPlayer (player) {
})
update()
})
device.on('disconnect', () => {
device.on('disconnect', function () {
if (device !== ret.device) return
state.playing.location = 'local'
update()
})
}
function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
}, err => {
}, function (err) {
if (err) {
state.playing.location = 'local'
state.errors.push({
@@ -297,45 +352,56 @@ function dlnaPlayer (player) {
update()
})
}
function play (callback) {
ret.device.play(null, null, callback)
}
function pause (callback) {
ret.device.pause(callback)
}
function stop (callback) {
ret.device.stop(callback)
}
function status () {
ret.device.status(handleStatus)
}
function seek (time, callback) {
ret.device.seek(time, callback)
}
function volume (volume, callback) {
ret.device.volume(volume, err => {
ret.device.volume(volume, function (err) {
// quick volume update
state.playing.volume = volume
callback(err)
})
}
}
function handleStatus (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.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level
update()
}
// Start polling cast device state, whenever we're connected
function startStatusInterval () {
statusInterval = setInterval(() => {
statusInterval = setInterval(function () {
const player = getPlayer()
if (player) player.status()
}, 1000)
}
/*
* 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.
@@ -347,38 +413,48 @@ function toggleMenu (location) {
state.devices.castMenu = null
return
}
// Never cast to two devices at the same time
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
const player = getPlayer(location)
const devices = player ? player.getDevices() : []
if (devices.length === 0) {
throw new CastingError(`No ${location} devices available`)
}
// Show a menu
state.devices.castMenu = { location, devices }
}
function selectDevice (index) {
const { location, devices } = state.devices.castMenu
// Start casting
const player = getPlayer(location)
player.device = devices[index]
player.open()
// Poll the casting device's status every few seconds
startStatusInterval()
// Show the Connecting... screen
state.devices.castMenu = null
state.playing.castName = devices[index].name
state.playing.location = location + '-pending'
update()
}
// Stops casting, move video back to local screen
function stop () {
const player = getPlayer()
if (player) {
player.stop(() => {
player.stop(function () {
player.device = null
stoppedCasting()
})
@@ -387,6 +463,7 @@ function stop () {
stoppedCasting()
}
}
function stoppedCasting () {
state.playing.location = 'local'
state.playing.jumpToTime = Number.isFinite(state.playing.currentTime)
@@ -394,6 +471,7 @@ function stoppedCasting () {
: 0
update()
}
function getPlayer (location) {
if (location) {
return state.devices[location]
@@ -407,14 +485,17 @@ function getPlayer (location) {
return null
}
}
function play () {
const player = getPlayer()
if (player) player.play(castCallback)
}
function pause () {
const player = getPlayer()
if (player) player.pause(castCallback)
}
function setRate (rate) {
let player
let result = true
@@ -430,34 +511,17 @@ function setRate (rate) {
}
return result
}
function seek (time) {
const player = getPlayer()
if (player) { player.seek(time, castCallback) }
if (player) player.seek(time, castCallback)
}
function setVolume (volume) {
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)
}
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
function castCallback () {
console.log('%s callback: %o', state.playing.location, arguments)
}

View File

@@ -1,11 +1,20 @@
module.exports = {
dispatch,
dispatcher,
setDispatch
}
const dispatchers = {}
let _dispatch = () => { }
let _dispatch = function () {}
function setDispatch (dispatch) {
_dispatch = dispatch
}
function dispatch (...args) {
_dispatch(...args)
}
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler
// function. This prevents React from updating the listener functions on
@@ -14,23 +23,17 @@ function dispatcher (...args) {
const str = JSON.stringify(args)
let handler = dispatchers[str]
if (!handler) {
handler = dispatchers[str] = e => {
handler = dispatchers[str] = function (e) {
// Do not propagate click to elements below the button
e.stopPropagation()
if (e.currentTarget.classList.contains('disabled')) {
// Ignore clicks on disabled elements
return
}
dispatch(...args)
}
}
return handler
}
export { dispatch }
export { dispatcher }
export { setDispatch }
export default {
dispatch,
dispatcher,
setDispatch
}

View File

@@ -1,37 +1,35 @@
import ExtendableError from 'es6-error'
const ExtendableError = require('es6-error')
/* Generic errors */
class CastingError extends ExtendableError {
}
class PlaybackError extends ExtendableError {
}
class SoundError extends ExtendableError {
}
class TorrentError extends ExtendableError {
}
class CastingError extends ExtendableError {}
class PlaybackError extends ExtendableError {}
class SoundError extends ExtendableError {}
class TorrentError extends ExtendableError {}
/* Playback */
class UnplayableTorrentError extends PlaybackError {
constructor () { super('Can\'t play any files in torrent') }
}
class UnplayableFileError extends PlaybackError {
constructor () { super('Can\'t play that file') }
}
/* Sound */
class InvalidSoundNameError extends SoundError {
constructor (name) { super(`Invalid sound name: ${name}`) }
}
/* Torrent */
class TorrentKeyNotFoundError extends TorrentError {
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
}
export { CastingError }
export { PlaybackError }
export { SoundError }
export { TorrentError }
export { UnplayableTorrentError }
export { UnplayableFileError }
export { InvalidSoundNameError }
export { TorrentKeyNotFoundError }
export default {
module.exports = {
CastingError,
PlaybackError,
SoundError,

View File

@@ -1,13 +1,12 @@
const mediaExtensions = {
audio: [
'.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a',
'.m2a', '.m4a', '.mpc', '.m4b', '.mka', '.mp2', '.mp3', '.mpc', '.oga',
'.ogg', '.opus', '.spx', '.wma', '.wav', '.wv', '.wvp'
],
'.m4a', '.m4b', '.mp2', '.mp3', '.mpc', '.oga', '.ogg', '.opus',
'.spx', '.wma', '.wav', '.wv', '.wvp'],
video: [
'.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg',
'.ogv', '.webm', '.wmv', '.m2ts'
],
'.ogv', '.webm', '.wmv'],
image: ['.gif', '.jpg', '.jpeg', '.png']
}
export default mediaExtensions
module.exports = mediaExtensions

View File

@@ -1,8 +1,14 @@
/* eslint-disable camelcase */
import fs, { copyFileSync } from 'fs'
import path from 'path'
import semver from 'semver'
import config from '../../config.js'
module.exports = {
run
}
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
// needed, for example to deal with config.json format changes across versions
@@ -23,27 +29,17 @@ function run (state) {
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.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)
}
if (semver.lt(version, '0.21.1')) migrate_0_21_1(saved)
// Config is now on the new 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) {
saved.torrents.forEach(ts => {
const { copyFileSync } = require('fs')
const path = require('path')
saved.torrents.forEach(function (ts) {
const infoHash = ts.infoHash
// Replace torrentPath with torrentFileName
@@ -82,6 +78,7 @@ function migrate_0_7_0 (saved) {
delete ts.posterURL
ts.posterFileName = infoHash + extension
}
// Fix exception caused by incorrect file ordering.
// https://github.com/webtorrent/webtorrent-desktop/pull/604#issuecomment-222805214
delete ts.defaultPlayFileIndex
@@ -106,8 +103,8 @@ function migrate_0_11_0 (saved) {
}
}
async function migrate_0_12_0 (saved) {
const TorrentSummary = await import('./torrent-summary')
function migrate_0_12_0 (saved) {
const TorrentSummary = require('./torrent-summary')
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
@@ -124,7 +121,7 @@ async function migrate_0_12_0 (saved) {
'02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent',
'3ba219a8634bf7bae3d848192b2da75ae995589d.torrent'
]
saved.torrents.forEach(torrentSummary => {
saved.torrents.forEach(function (torrentSummary) {
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
if (!fileOrFolder) return
@@ -138,16 +135,16 @@ async function migrate_0_12_0 (saved) {
}
function migrate_0_14_0 (saved) {
saved.torrents.forEach(ts => {
saved.torrents.forEach(function (ts) {
delete ts.defaultPlayFileIndex
})
}
function migrate_0_17_0 (saved) {
// Fix a sad, sad bug that resulted in 100MB+ config.json files
saved.torrents.forEach(ts => {
saved.torrents.forEach(function (ts) {
if (!ts.files) return
ts.files.forEach(file => {
ts.files.forEach(function (file) {
if (!file.audioInfo || !file.audioInfo.picture) return
// This contained a Buffer, which 30x'd in size when serialized to JSON
delete file.audioInfo.picture
@@ -159,13 +156,19 @@ function migrate_0_17_2 (saved) {
// 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
// Windows Explorer. See: https://github.com/webtorrent/webtorrent-desktop/issues/905
const { copyFileSync } = require('fs')
const rimraf = require('rimraf')
const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.'
const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share'
const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d'
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
@@ -188,7 +191,7 @@ function migrate_0_17_2 (saved) {
} catch (err) {}
ts.posterFileName = NEW_HASH + '.jpg'
fs.rmdirSync(path.join(config.TORRENT_PATH, ts.torrentFileName))
rimraf.sync(path.join(config.TORRENT_PATH, ts.torrentFileName))
copyFileSync(
path.join(config.STATIC_PATH, 'wiredCd.torrent'),
path.join(config.TORRENT_PATH, NEW_HASH + '.torrent')
@@ -213,12 +216,8 @@ function migrate_0_21_0 (saved) {
}
}
function migrate_0_22_0 (saved) {
function migrate_0_21_1 (saved) {
if (saved.prefs.externalPlayerPath == null) {
saved.prefs.externalPlayerPath = ''
}
}
export default {
run
}

View File

@@ -1,36 +1,52 @@
import TorrentSummary from './torrent-summary.js'
import TorrentPlayer from './torrent-player.js'
module.exports = {
hasNext,
getNextIndex,
hasPrevious,
getPreviousIndex,
getCurrentLocalURL
}
const TorrentSummary = require('./torrent-summary')
const TorrentPlayer = require('./torrent-player')
const cache = {
infoHash: null,
previousIndex: null,
currentIndex: null,
nextIndex: null
}
function hasNext (state) {
updateCache(state)
return cache.nextIndex !== null
}
function getNextIndex (state) {
updateCache(state)
return cache.nextIndex
}
function hasPrevious (state) {
updateCache(state)
return cache.previousIndex !== null
}
function getPreviousIndex (state) {
updateCache(state)
return cache.previousIndex
}
function getCurrentLocalURL (state) {
return state.server
? state.server.localURL + '/' + state.playing.fileIndex + '/' +
encodeURIComponent(state.playing.fileName)
encodeURIComponent(state.playing.fileName)
: ''
}
function updateCache (state) {
const infoHash = state.playing.infoHash
const fileIndex = state.playing.fileIndex
if (infoHash === cache.infoHash) {
switch (fileIndex) {
case cache.currentIndex:
@@ -49,10 +65,12 @@ function updateCache (state) {
} else {
cache.infoHash = infoHash
}
cache.previousIndex = findPreviousIndex(state)
cache.currentIndex = fileIndex
cache.nextIndex = findNextIndex(state)
}
function findPreviousIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (let i = state.playing.fileIndex - 1; i >= 0; i--) {
@@ -60,6 +78,7 @@ function findPreviousIndex (state) {
}
return null
}
function findNextIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (let i = state.playing.fileIndex + 1; i < files.length; i++) {
@@ -67,15 +86,3 @@ function findNextIndex (state) {
}
return null
}
export { hasNext }
export { getNextIndex }
export { hasPrevious }
export { getPreviousIndex }
export { getCurrentLocalURL }
export default {
hasNext,
getNextIndex,
hasPrevious,
getPreviousIndex,
getCurrentLocalURL
}

View File

@@ -1,11 +1,20 @@
import config from '../src/config.js'
import { InvalidSoundNameError } from './errors.js'
import path from 'path'
module.exports = {
init,
play
}
const config = require('../../config')
const { InvalidSoundNameError } = require('./errors')
const path = require('path')
const VOLUME = 0.25
// App state to access the soundNotifications preference
let state
/* Cache of Audio elements, for instant playback */
const cache = {}
const sounds = {
ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
@@ -40,16 +49,20 @@ const sounds = {
volume: VOLUME
}
}
function init (appState) {
state = appState
}
function play (name) {
if (state == null) {
return
}
if (!state.saved.prefs.soundNotifications) {
return
}
let audio = cache[name]
if (!audio) {
const sound = sounds[name]
@@ -63,9 +76,3 @@ function play (name) {
audio.currentTime = 0
audio.play()
}
export { init }
export { play }
export default {
init,
play
}

View File

@@ -1,33 +1,30 @@
import applicationConfig from 'application-config'
import path from 'path'
import { EventEmitter } from 'events'
const appConfig = require('application-config')('WebTorrent')
const path = require('path')
const { EventEmitter } = require('events')
import config from '../../config.js'
const config = require('../../config')
const appConfig = applicationConfig('WebTorrent')
const SAVE_DEBOUNCE_INTERVAL = 1000
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
const State = Object.assign(new EventEmitter(), {
const State = module.exports = Object.assign(new EventEmitter(), {
getDefaultPlayState,
load,
// 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)
const debounce = await import('debounce')
const debounce = require('debounce')
// After first State.save() invokation, future calls go straight to the
// debounced function
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
State.save(...args)
State.save(...arguments)
},
saveImmediate
})
export default State
async function getDefaultState () {
const { default: LocationHistory } = await import('location-history')
function getDefaultState () {
const LocationHistory = require('location-history')
return {
/*
@@ -108,19 +105,14 @@ function getDefaultPlayState () {
selectedIndex: -1, /* current subtitle track */
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 */
}
}
/* If the saved state file doesn't exist yet, here's what we use instead */
async function setupStateSaved () {
const { copyFileSync, mkdirSync, readFileSync } = await import('fs')
const { default: parseTorrent } = await import('parse-torrent')
function setupStateSaved () {
const { copyFileSync, mkdirSync, readFileSync } = require('fs')
const parseTorrent = require('parse-torrent')
const saved = {
prefs: {
@@ -205,25 +197,23 @@ function shouldHidePlayerControls () {
this.playing.playbackRate === 1
}
async function load (cb) {
console.log('state load')
let saved = await appConfig.read()
if (!saved || !saved.version) {
console.log('Missing config file: Creating new one')
try {
saved = await setupStateSaved()
} catch (err) {
onSavedState(err)
return
function load (cb) {
appConfig.read(function (err, saved) {
if (err || !saved.version) {
console.log('Missing config file: Creating new one')
try {
saved = setupStateSaved()
} catch (err) {
onSavedState(err)
return
}
}
}
onSavedState(null, saved)
})
onSavedState(null, saved)
async function onSavedState (err, saved) {
function onSavedState (err, saved) {
if (err) return cb(err)
const state = await getDefaultState()
const state = getDefaultState()
state.saved = saved
if (process.type === 'renderer') {
@@ -232,13 +222,12 @@ async function load (cb) {
migrations.run(state)
}
console.log('calling cb')
cb(null, state)
}
}
// Write state.saved to the JSON state file
async function saveImmediate (state, cb) {
function saveImmediate (state, cb) {
console.log('Saving state to ' + appConfig.filePath)
// Clean up, so that we're not saving any pending state
@@ -247,7 +236,7 @@ async function saveImmediate (state, cb) {
// reading the torrent file or file(s) to seed & don't have an infohash
copy.torrents = copy.torrents
.filter((x) => x.infoHash)
.map(x => {
.map(function (x) {
const torrent = {}
for (const key in x) {
if (key === 'progress' || key === 'torrentKey') {
@@ -261,10 +250,8 @@ async function saveImmediate (state, cb) {
return torrent
})
try {
await appConfig.write(copy)
State.emit('stateSaved')
} catch (err) {
console.error(err)
}
appConfig.write(copy, (err) => {
if (err) console.error(err)
else State.emit('stateSaved')
})
}

View File

@@ -1,14 +1,24 @@
import remote from '@electron/remote'
import config from '../../config.js'
// Collects anonymous usage stats and uncaught errors
// 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
async function init (state) {
function init (state) {
telemetry = state.saved.telemetry
// First app run
if (!telemetry) {
const crypto = await import('crypto')
const crypto = require('crypto')
telemetry = state.saved.telemetry = {
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()
telemetry.version = config.APP_VERSION
telemetry.timestamp = now.toISOString()
@@ -32,7 +42,7 @@ async function send (state) {
return reset()
}
const get = await import('simple-get')
const get = require('simple-get')
const opts = {
url: config.TELEMETRY_URL,
@@ -40,7 +50,7 @@ async function send (state) {
json: true
}
get.post(opts, (err, res) => {
get.post(opts, function (err, res) {
if (err) return console.error('Error sending telemetry', err)
if (res.statusCode !== 200) {
return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
@@ -64,7 +74,7 @@ function reset () {
// Track screen resolution
function getScreenInfo () {
return remote.screen.getAllDisplays().map((screen) => ({
return electron.remote.screen.getAllDisplays().map((screen) => ({
width: screen.size.width,
height: screen.size.height,
scaleFactor: screen.scaleFactor
@@ -72,8 +82,8 @@ function getScreenInfo () {
}
// Track basic system info like OS version and amount of RAM
async function getSystemInfo () {
const os = await import('os')
function getSystemInfo () {
const os = require('os')
return {
osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(),
@@ -96,14 +106,15 @@ function getTorrentStats (state) {
}
// First, count torrents & total file size
for (const torrent of state.saved.torrents) {
const stat = byStatus[torrent.status]
if (!torrent || !torrent.files || !stat) continue
for (let i = 0; i < count; i++) {
const t = state.saved.torrents[i]
const stat = byStatus[t.status]
if (!t || !t.files || !stat) continue
stat.count++
for (const file of torrent.files) {
if (!file || !file.length) continue
const fileSizeMB = file.length / (1 << 20)
for (let j = 0; j < t.files.length; j++) {
const f = t.files[j]
if (!f || !f.length) continue
const fileSizeMB = f.length / (1 << 20)
sizeMB += fileSizeMB
stat.sizeMB += fileSizeMB
}
@@ -134,7 +145,7 @@ function roundPow2 (n) {
if (n <= 0) return 0
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
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
@@ -210,14 +221,3 @@ function logPlayAttempt (result) {
attempts.total = (attempts.total || 0) + 1
attempts[result] = (attempts[result] || 0) + 1
}
export { init }
export { send }
export { logUncaughtError }
export { logPlayAttempt }
export default {
init,
send,
logUncaughtError,
logPlayAttempt
}

View File

@@ -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
}

View File

@@ -1,45 +1,4 @@
import path from 'path'
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 {
module.exports = {
isPlayable,
isVideo,
isAudio,
@@ -47,3 +6,48 @@ export default {
isMagnetLink,
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)
}

View File

@@ -1,20 +1,34 @@
import captureFrame from 'capture-frame'
import path from 'path'
import mediaExtensions from './media-extensions.js'
module.exports = torrentPoster
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'
function torrentPoster (torrent, cb) {
// 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)
// 'score' each media type based on total size present in torrent
const bestScore = ['audio', 'video', 'image'].map(mediaType => ({
type: mediaType,
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])
})).sort((a, b) => b.size - a.size)[0] // sort descending on size
const bestScore = ['audio', 'video', 'image'].map(mediaType => {
return {
type: mediaType,
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])
}
}).sort((a, b) => { // sort descending on size
return b.size - a.size
})[0]
if (bestScore.size === 0) {
// Admit defeat, no video, audio or image had a significant presence
return cb(new Error(msgNoSuitablePoster))
}
// Based on which media type is dominant we select the corresponding poster function
switch (bestScore.type) {
case 'audio':
@@ -25,6 +39,7 @@ function torrentPoster (torrent, cb) {
return torrentPosterFromVideo(torrent, cb)
}
}
/**
* Calculate the total data size of file matching one of the provided extensions
* @param torrent
@@ -36,8 +51,11 @@ function calculateDataLengthByExtension (torrent, extensions) {
if (files.length === 0) return 0
return files
.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
* @param torrent Torrent to search in
@@ -47,13 +65,16 @@ function calculateDataLengthByExtension (torrent, extensions) {
function getLargestFileByExtension (torrent, extensions) {
const files = filterOnExtension(torrent, extensions)
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
* @param torrent Torrent to filter files from
* @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) {
return torrent.files.filter(file => {
@@ -61,6 +82,7 @@ function filterOnExtension (torrent, extensions) {
return extensions.indexOf(extname) !== -1
})
}
/**
* Returns a score how likely the file is suitable as a poster
* @param imgFile File object of an image
@@ -76,6 +98,7 @@ function scoreAudioCoverFile (imgFile) {
back: 20,
spectrogram: -80
}
for (const keyword in relevanceScore) {
if (fileName === keyword) {
return relevanceScore[keyword]
@@ -86,13 +109,18 @@ function scoreAudioCoverFile (imgFile) {
}
return 0
}
function torrentPosterFromAudio (torrent, cb) {
const imageFiles = filterOnExtension(torrent, mediaExtensions.image)
if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster))
const bestCover = imageFiles.map(file => ({
file,
score: scoreAudioCoverFile(file)
})).reduce((a, b) => {
const bestCover = imageFiles.map(file => {
return {
file: file,
score: scoreAudioCoverFile(file)
}
}).reduce((a, b) => {
if (a.score > b.score) {
return a
}
@@ -105,47 +133,61 @@ function torrentPosterFromAudio (torrent, cb) {
}
return b
})
const extname = path.extname(bestCover.file.name)
bestCover.file.getBuffer((err, buf) => cb(err, buf, extname))
}
function torrentPosterFromVideo (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.video)
const index = torrent.files.indexOf(file)
const server = torrent.createServer(0)
server.listen(0, onListening)
function onListening () {
const port = server.address().port
const url = 'http://localhost:' + port + '/' + index
const video = document.createElement('video')
video.addEventListener('canplay', onCanPlay)
video.volume = 0
video.src = url
video.play()
function onCanPlay () {
video.removeEventListener('canplay', onCanPlay)
video.addEventListener('seeked', onSeeked)
video.currentTime = Math.min((video.duration || 600) * 0.03, 60)
}
function onSeeked () {
video.removeEventListener('seeked', onSeeked)
const frame = captureFrame(video)
const buf = frame && frame.image
const buf = captureFrame(video)
// unload video element
video.pause()
video.src = ''
video.load()
server.destroy()
if (buf.length === 0) return cb(new Error(msgNoSuitablePoster))
cb(null, buf, '.jpg')
}
}
}
function torrentPosterFromImage (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.image)
extractPoster(file, cb)
}
function extractPoster (file, cb) {
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

View File

@@ -1,11 +1,21 @@
import path from 'path'
import config from '../src/config.js'
module.exports = {
getPosterPath,
getTorrentPath,
getByKey,
getTorrentId,
getFileOrFolder
}
const path = require('path')
const config = require('../../config')
// Expects a torrentSummary
// Returns an absolute path to the torrent file, or null if unavailable
function getTorrentPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.torrentFileName) return null
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
}
// Expects a torrentSummary
// Returns an absolute path to the poster image, or null if unavailable
function getPosterPath (torrentSummary) {
@@ -15,6 +25,7 @@ function getPosterPath (torrentSummary) {
// Backslashes in URLS in CSS cause bizarre string encoding issues
return posterPath.replace(/\\/g, '/')
}
// Expects a torrentSummary
// Returns a torrentID: filename, magnet URI, or infohash
function getTorrentId (torrentSummary) {
@@ -25,12 +36,15 @@ function getTorrentId (torrentSummary) {
return s.magnetURI || s.infoHash
}
}
// Expects a torrentKey or infoHash
// Returns the corresponding torrentSummary, or undefined
function getByKey (state, torrentKey) {
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
// folder (in multi-file torrent)
// 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]
return path.join(ts.path, dirname)
}
export { getPosterPath }
export { getTorrentPath }
export { getByKey }
export { getTorrentId }
export { getFileOrFolder }
export default {
getPosterPath,
getTorrentPath,
getByKey,
getTorrentId,
getFileOrFolder
}

View File

@@ -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')
// Perf optimization: Start asynchronously read on config file before all the
// blocking import() calls below.
const crashReporter = require('../crash-reporter')
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)
import createGetter from 'fn-getter'
import remote from '@electron/remote'
import debounce from 'debounce'
import dragDrop from 'drag-drop'
import electron from 'electron'
import fs from 'fs'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
const createGetter = require('fn-getter')
const debounce = require('debounce')
const dragDrop = require('drag-drop')
const electron = require('electron')
const fs = require('fs')
const React = require('react')
const ReactDOM = require('react-dom')
import config from '../config.js'
import telemetry from './lib/telemetry.js'
import sound from './lib/sound.js'
import TorrentPlayer from './lib/torrent-player.js'
import App from './pages/app.js'
import { setDispatch } from './lib/dispatcher.js'
const config = require('../config')
const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound')
const TorrentPlayer = require('./lib/torrent-player')
import TorrentController from './controllers/torrent-controller.js'
import AudioTracksController from './controllers/audio-tracks-controller.js'
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'
// Perf optimization: Needed immediately, so do not lazy load it below
const TorrentListController = require('./controllers/torrent-list-controller')
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.
// 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
let controllers = null
@@ -72,15 +83,37 @@ function onState (err, _state) {
// Create controllers
controllers = {
media: createGetter(() => new MediaController(state)),
playback: createGetter(() => new PlaybackController(state, config, update)),
prefs: createGetter(() => new PrefsController(state, config)),
subtitles: createGetter(() => new SubtitlesController(state)),
audioTracks: createGetter(() => new AudioTracksController(state)),
torrent: createGetter(() => new TorrentController(state)),
torrentList: createGetter(() => new TorrentListController(state)),
update: createGetter(() => new UpdateController(state)),
folderWatcher: createGetter(() => new FolderWatcherController())
media: createGetter(() => {
const MediaController = require('./controllers/media-controller')
return new MediaController(state)
}),
playback: createGetter(() => {
const PlaybackController = require('./controllers/playback-controller')
return new PlaybackController(state, config, update)
}),
prefs: createGetter(() => {
const PrefsController = require('./controllers/prefs-controller')
return new PrefsController(state, config)
}),
subtitles: createGetter(() => {
const SubtitlesController = require('./controllers/subtitles-controller')
return new SubtitlesController(state)
}),
torrent: createGetter(() => {
const TorrentController = require('./controllers/torrent-controller')
return new TorrentController(state)
}),
torrentList: createGetter(() => {
return new TorrentListController(state)
}),
update: createGetter(() => {
const UpdateController = require('./controllers/update-controller')
return new UpdateController(state)
}),
folderWatcher: createGetter(() => {
const FolderWatcherController = require('./controllers/folder-watcher-controller')
return new FolderWatcherController()
})
}
// Add first page to location history
@@ -96,10 +129,7 @@ function onState (err, _state) {
resumeTorrents()
// Initialize ReactDOM
ReactDOM.render(
<App state={state} ref={elem => { app = elem }} />,
document.querySelector('#body')
)
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
// Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary
@@ -121,11 +151,11 @@ function onState (err, _state) {
// Add YouTube style hotkey shortcuts
window.addEventListener('keydown', onKeydown)
const debouncedFullscreenToggle = debounce(() => {
const debouncedFullscreenToggle = debounce(function () {
dispatch('toggleFullScreen')
}, 1000, true)
document.addEventListener('wheel', event => {
document.addEventListener('wheel', function (event) {
// ctrlKey detects pinch to zoom, http://crbug.com/289887
if (event.ctrlKey) {
event.preventDefault()
@@ -137,7 +167,7 @@ function onState (err, _state) {
window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur)
if (remote.getCurrentWindow().isVisible()) {
if (electron.remote.getCurrentWindow().isVisible()) {
sound.play('STARTUP')
}
@@ -167,9 +197,9 @@ function delayedInit () {
}
// Lazily loads Chromecast and Airplay support
async function lazyLoadCast () {
function lazyLoadCast () {
if (!Cast) {
Cast = (await import('./lib/cast')).default
Cast = require('./lib/cast')
Cast.init(state, update) // Search the local network for Chromecast and Airplays
}
return Cast
@@ -221,11 +251,6 @@ const dispatchHandlers = {
controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
deleteTorrent: (infoHash, deleteData) =>
controllers.torrentList().deleteTorrent(infoHash, deleteData),
openTorrentListContextMenu: () => onPaste(),
confirmDeleteAllTorrents: (deleteData) =>
controllers.torrentList().confirmDeleteAllTorrents(deleteData),
deleteAllTorrents: (deleteData) =>
controllers.torrentList().deleteAllTorrents(deleteData),
toggleSelectTorrent: (infoHash) =>
controllers.torrentList().toggleSelectTorrent(infoHash),
openTorrentContextMenu: (infoHash) =>
@@ -244,12 +269,10 @@ const dispatchHandlers = {
previousTrack: () => controllers.playback().previousTrack(),
skip: (time) => controllers.playback().skip(time),
skipTo: (time) => controllers.playback().skipTo(time),
preview: (x) => controllers.playback().preview(x),
clearPreview: () => controllers.playback().clearPreview(),
changePlaybackRate: (dir) => controllers.playback().changePlaybackRate(dir),
changeVolume: (delta) => controllers.playback().changeVolume(delta),
setVolume: (vol) => controllers.playback().setVolume(vol),
openPath: (infoHash, index) => controllers.playback().openPath(infoHash, index),
openItem: (infoHash, index) => controllers.playback().openItem(infoHash, index),
// Subtitles
openSubtitles: () => controllers.subtitles().openSubtitles(),
@@ -258,10 +281,6 @@ const dispatchHandlers = {
checkForSubtitles: () => controllers.subtitles().checkForSubtitles(),
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
mediaStalled: () => controllers.media().mediaStalled(),
mediaError: (err) => controllers.media().mediaError(err),
@@ -281,7 +300,7 @@ const dispatchHandlers = {
// Preferences screen
preferences: () => controllers.prefs().show(),
updatePreferences: (key, value) => controllers.prefs().update(key, value),
checkDownloadPath,
checkDownloadPath: checkDownloadPath,
startFolderWatcher: () => controllers.folderWatcher().start(),
stopFolderWatcher: () => controllers.folderWatcher().stop(),
@@ -291,20 +310,20 @@ const dispatchHandlers = {
// Navigation between screens (back, forward, ESC, etc)
exitModal: () => { state.modal = null },
backToList,
escapeBack,
backToList: backToList,
escapeBack: escapeBack,
back: () => state.location.back(),
forward: () => state.location.forward(),
cancel: () => state.location.cancel(),
// Controlling the window
setDimensions,
setDimensions: setDimensions,
toggleFullScreen: (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
setTitle: (title) => { state.window.title = title },
resetTitle: () => { state.window.title = config.APP_WINDOW_TITLE },
// Everything else
onOpen,
onOpen: onOpen,
error: onError,
uncaughtError: (proc, err) => telemetry.logUncaughtError(proc, err),
stateSave: () => State.save(state),
@@ -366,7 +385,7 @@ function setupIpc () {
function backToList () {
// Exit any modals and screens with a back button
state.modal = null
state.location.backToFirst(() => {
state.location.backToFirst(function () {
// If we were already on the torrent list, scroll to the top
const contentTag = document.querySelector('.content')
if (contentTag) contentTag.scrollTop = 0
@@ -400,7 +419,7 @@ function resumeTorrents () {
// Set window dimensions to match video dimensions or fill the screen
function setDimensions (dimensions) {
// Don't modify the window size if it's already maximized
if (remote.getCurrentWindow().isMaximized()) {
if (electron.remote.getCurrentWindow().isMaximized()) {
state.window.bounds = null
return
}
@@ -412,7 +431,7 @@ function setDimensions (dimensions) {
width: window.outerWidth,
height: window.outerHeight
}
state.window.wasMaximized = remote.getCurrentWindow().isMaximized
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
// Limit window size to screen size
const screenWidth = window.screen.width
@@ -486,16 +505,13 @@ function onError (err) {
const editableHtmlTags = new Set(['input', 'textarea'])
function onPaste (e) {
if (e && editableHtmlTags.has(e.target.tagName.toLowerCase())) return
controllers.torrentList().addTorrent(clipboard.readText())
if (editableHtmlTags.has(e.target.tagName.toLowerCase())) return
controllers.torrentList().addTorrent(electron.clipboard.readText())
update()
}
function onKeydown (e) {
// prevent event fire on user input elements
if (editableHtmlTags.has(e.target.tagName.toLowerCase())) return
const key = e.key
if (key === 'ArrowLeft') {
@@ -535,7 +551,7 @@ function onBlur () {
}
function onVisibilityChange () {
state.window.isVisible = !document.hidden
state.window.isVisible = !document.webkitHidden
}
function onFullscreenChanged (e, isFullScreen) {
@@ -556,7 +572,7 @@ function onWindowBoundsChanged (e, newBounds) {
}
function checkDownloadPath () {
fs.stat(state.saved.prefs.downloadPath, (err, stat) => {
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
if (err) {
state.downloadPathStatus = 'missing'
return console.error(err)

View File

@@ -1,35 +1,30 @@
import * as colors from 'material-ui/styles/colors'
import createGetter from 'fn-getter'
import * as React from 'react'
import darkBaseTheme$0 from 'material-ui/styles/baseThemes/darkBaseTheme'
import getMuiTheme$0 from 'material-ui/styles/getMuiTheme'
import muiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import Header from '../components/header.js'
import TorrentListPage from './torrent-list-page.js'
import playerPage from './player-page.js'
import createTorrentPage from './create-torrent-page.js'
import preferencesPage from './preferences-page.js'
import openTorrentAddressModal from '../components/open-torrent-address-modal.js'
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 colors = require('material-ui/styles/colors')
const createGetter = require('fn-getter')
const React = require('react')
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
const Header = require('../components/header')
// Perf optimization: Needed immediately, so do not lazy load it below
const TorrentListPage = require('./torrent-list-page')
const Views = {
home: createGetter(() => TorrentListPage),
player: createGetter(() => playerPage),
'create-torrent': createGetter(() => createTorrentPage),
preferences: createGetter(() => preferencesPage)
player: createGetter(() => require('./player-page')),
'create-torrent': createGetter(() => require('./create-torrent-page')),
preferences: createGetter(() => require('./preferences-page'))
}
const Modals = {
'open-torrent-address-modal': createGetter(() => openTorrentAddressModal),
'remove-torrent-modal': createGetter(() => removeTorrentModal),
'update-available-modal': createGetter(() => updateAvailableModal),
'unsupported-media-modal': createGetter(() => unsupportedMediaModal),
'delete-all-torrents-modal': createGetter(() => deleteAllTorrentsModal)
'open-torrent-address-modal': createGetter(
() => require('../components/open-torrent-address-modal')
),
'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')),
'update-available-modal': createGetter(() => require('../components/update-available-modal')),
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
}
const fontFamily = process.platform === 'win32'
@@ -48,7 +43,7 @@ darkBaseTheme.palette.accent3Color = colors.redA100
let darkMuiTheme
let lightMuiTheme
export default class App extends React.Component {
class App extends React.Component {
render () {
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 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 (
<div
key='errors'
@@ -101,12 +98,12 @@ export default class App extends React.Component {
)
}
async getModal () {
getModal () {
const state = this.props.state
if (!state.modal) return
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.userAgent = false
lightMuiTheme = getMuiTheme(lightBaseTheme)
@@ -131,3 +128,5 @@ export default class App extends React.Component {
return (<View state={state} />)
}
}
module.exports = App

View File

@@ -1,25 +1,22 @@
import createTorrent from 'create-torrent'
import path from 'path'
import prettyBytes from 'prettier-bytes'
import * as React from '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'
const createTorrent = require('create-torrent')
const path = require('path')
const prettyBytes = require('prettier-bytes')
const React = require('react')
import { dispatch, dispatcher } from '../lib/dispatcher.js'
import CreateTorrentErrorPage from '../components/create-torrent-error-page.js'
import Heading from '../components/heading.js'
import ShowMore from '../components/show-more.js'
const { dispatch, dispatcher } = require('../lib/dispatcher')
const FlatButton = flatButton.default
const RaisedButton = raisedButton.default
const TextField = textField.default
const Checkbox = checkbox.default
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
const Checkbox = require('material-ui/Checkbox').default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/heading')
const ShowMore = require('../components/show-more')
// Shows a basic UI to create a torrent, from an already-selected file or folder.
// Includes a "Show Advanced..." button and more advanced UI.
export default class CreateTorrentPage extends React.Component {
class CreateTorrentPage extends React.Component {
constructor (props) {
super(props)
@@ -226,3 +223,5 @@ function containsDots (path, pathPrefix) {
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
return ('/' + suffix).includes('/.')
}
module.exports = CreateTorrentPage

View File

@@ -1,16 +1,14 @@
import * as React from 'react'
import bitfield from 'bitfield'
import prettyBytes from 'prettier-bytes'
const React = require('react')
const Bitfield = require('bitfield')
const prettyBytes = require('prettier-bytes')
import TorrentSummary from '../lib/torrent-summary.js'
import Playlist from '../lib/playlist.js'
import { dispatch, dispatcher } from '../lib/dispatcher.js'
import config from '../../config.js'
import { calculateEta } from '../lib/time.js'
const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist')
const { dispatch, dispatcher } = require('../lib/dispatcher')
const config = require('../../config')
const BitField = bitfield.default
export default class Player extends React.Component {
// Shows a streaming video player. Standard features + Chromecast + Airplay
module.exports = class Player extends React.Component {
render () {
// Show the video as large as will fit in the window, play immediately
// If the video is on Chromecast or Airplay, show a title screen instead
@@ -56,6 +54,10 @@ function renderMedia (state) {
mediaElement.pause()
} else if (!state.playing.isPaused && mediaElement.paused) {
mediaElement.play()
.then(
() => dispatch('mediaSuccess'),
err => dispatch('mediaError', err.name === 'NotSupportedError' ? 'Codec unsupported' : `${err.name}: ${err.message}`)
)
}
// When the user clicks or drags on the progress bar, jump to that position
if (state.playing.jumpToTime != null) {
@@ -97,31 +99,25 @@ function renderMedia (state) {
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
}
// Add subtitles to the <video> tag
const trackTags = []
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
trackTags.push(
<track
key={i}
default={isSelected}
default={isSelected ? 'default' : ''}
label={track.label}
kind='subtitles'
type='subtitles'
src={track.buffer}
/>
)
})
}
}
// Create the <audio> or <video> tag
@@ -130,11 +126,9 @@ function renderMedia (state) {
<MediaTagName
src={Playlist.getCurrentLocalURL(state)}
onDoubleClick={dispatcher('toggleFullScreen')}
onClick={dispatcher('playPause')}
onLoadedMetadata={onLoadedMetadata}
onEnded={onEnded}
onStalled={dispatcher('mediaStalled')}
onError={dispatcher('mediaError')}
onTimeUpdate={dispatcher('mediaTimeUpdate')}
onEncrypted={dispatcher('mediaEncrypted')}
>
@@ -154,53 +148,18 @@ function renderMedia (state) {
</div>
)
// As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) {
const mediaElement = e.target
// check if we can decode video and audio track
if (state.playing.type === 'video') {
if (mediaElement.videoTracks.length === 0) {
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')
if (state.playing.type !== 'video') return
const video = e.target
const dimensions = {
width: video.videoWidth,
height: video.videoHeight
}
dispatch('setDimensions', dimensions)
}
function onEnded () {
function onEnded (e) {
if (Playlist.hasNext(state)) {
dispatch('nextTrack')
} else {
@@ -342,13 +301,13 @@ function renderAudioMetadata (state) {
format.push(fileSummary.audioInfo.format.codec)
}
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) {
format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz')
}
if (fileSummary.audioInfo.format.bitsPerSample) {
format.push(fileSummary.audioInfo.format.bitsPerSample + '-bit')
format.push(fileSummary.audioInfo.format.bitsPerSample + ' bit')
}
if (format.length > 0) {
elems.push((
@@ -429,70 +388,22 @@ function renderCastScreen (state) {
const isStarting = state.playing.location.endsWith('-pending')
const castName = state.playing.castName
const fileName = state.getPlayingFileSummary().name || ''
let castStatus
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
else castStatus = ''
const prog = state.getPlayingTorrentSummary().progress || {}
// Show a nice title image, if possible
const style = {
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 (
<div key='cast' className='letterbox' style={style}>
<div className='cast-screen'>
<i className='icon'>{castIcon}</i>
<div key='type' className='cast-type'>{castType}</div>
<div key='status' className='cast-status'>{castStatus}</div>
<div key='name' className='name'>{fileName}</div>
{renderDownloadProgress()}
</div>
</div>
)
@@ -504,7 +415,7 @@ function renderCastOptions (state) {
const { location, devices } = state.devices.castMenu
const player = state.devices[location]
const items = devices.map((device, ix) => {
const items = devices.map(function (device, ix) {
const isSelected = player.device === device
const name = device.name
return (
@@ -527,7 +438,7 @@ function renderSubtitleOptions (state) {
const subtitles = state.playing.subtitles
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
return (
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
@@ -550,27 +461,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) {
const positionPercent = 100 * state.playing.currentTime / state.playing.duration
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
@@ -579,15 +469,10 @@ function renderPlayerControls (state) {
: state.playing.subtitles.selectedIndex >= 0
? 'active'
: ''
const multiAudioClass = state.playing.audioTracks.tracks.length > 1
? 'active'
: 'disabled'
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
const elements = [
renderPreview(state),
<div key='playback-bar' className='playback-bar'>
{renderLoadingBar(state)}
<div
@@ -599,8 +484,6 @@ function renderPlayerControls (state) {
key='scrub-bar'
className='scrub-bar'
draggable='true'
onMouseMove={handleScrubPreview}
onMouseOut={clearPreview}
onDragStart={handleDragStart}
onClick={handleScrub}
onDrag={handleScrub}
@@ -650,14 +533,6 @@ function renderPlayerControls (state) {
>
closed_caption
</i>
), (
<i
key='audio-tracks'
className={'icon multi-audio float-right ' + multiAudioClass}
onClick={handleAudioTracks}
>
library_music
</i>
))
}
@@ -672,7 +547,7 @@ function renderPlayerControls (state) {
airplay: { true: 'airplay', false: 'airplay' },
dlna: { true: 'tv', false: 'tv' }
}
castTypes.forEach(castType => {
castTypes.forEach(function (castType) {
// Do we show this button (eg. the Chromecast button) at all?
const isCasting = state.playing.location.startsWith(castType)
const player = state.devices[castType]
@@ -709,14 +584,10 @@ function renderPlayerControls (state) {
// Render volume slider
const volume = state.playing.volume
const volumeIcon = 'volume_' + (
volume === 0
? 'off'
: volume < 0.3
? 'mute'
: volume < 0.6
? 'down'
: 'up'
)
volume === 0 ? 'off'
: volume < 0.3 ? 'mute'
: volume < 0.6 ? 'down'
: 'up')
const volumeStyle = {
background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' +
@@ -759,31 +630,26 @@ function renderPlayerControls (state) {
))
}
const emptyImage = new window.Image(0, 0)
emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'
return (
<div
key='controls' className='controls'
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
>
{elements}
{renderCastOptions(state)}
{renderSubtitleOptions(state)}
</div>
)
function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) {
const dt = e.dataTransfer
// Prevent the cursor from changing, eg to a green + icon on Mac
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)
function handleScrub (e) {
if (!e.clientX) return
@@ -795,7 +661,7 @@ function renderPlayerControls (state) {
}
// Handles volume muting and Unmuting
function handleVolumeMute () {
function handleVolumeMute (e) {
if (state.playing.volume === 0.0) {
dispatch('setVolume', 1.0)
} else {
@@ -816,73 +682,6 @@ function renderPlayerControls (state) {
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
@@ -904,7 +703,7 @@ function renderLoadingBar (state) {
const parts = []
let lastPiecePresent = false
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) {
parts.push({ start: i - fileProg.startPiece, count: 1 })
} else if (partPresent) {
@@ -914,7 +713,7 @@ function renderLoadingBar (state) {
}
// 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 = {
left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / fileProg.numPieces) + '%'

View File

@@ -1,18 +1,16 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import * as colors from 'material-ui/styles/colors'
import checkbox from 'material-ui/Checkbox'
import raisedButton from 'material-ui/RaisedButton'
const React = require('react')
const PropTypes = require('prop-types')
import Heading from '../components/heading.js'
import PathSelector from '../components/path-selector.js'
import { dispatch } from '../lib/dispatcher.js'
import config from '../../config.js'
const colors = require('material-ui/styles/colors')
const Checkbox = require('material-ui/Checkbox').default
const RaisedButton = require('material-ui/RaisedButton').default
const Heading = require('../components/heading')
const PathSelector = require('../components/path-selector')
const Checkbox = checkbox.default
const RaisedButton = raisedButton.default
const { dispatch } = require('../lib/dispatcher')
const config = require('../../config')
export default class PreferencesPage extends React.Component {
class PreferencesPage extends React.Component {
constructor (props) {
super(props)
@@ -288,3 +286,5 @@ class Preference extends React.Component {
return (<div style={style}>{this.props.children}</div>)
}
}
module.exports = PreferencesPage

View File

@@ -1,17 +1,14 @@
import * as React from 'react'
import prettyBytes from 'prettier-bytes'
import checkbox from 'material-ui/Checkbox'
import linearProgress from 'material-ui/LinearProgress'
const React = require('react')
const prettyBytes = require('prettier-bytes')
import TorrentSummary from '../lib/torrent-summary.js'
import TorrentPlayer from '../lib/torrent-player.js'
import { dispatcher } from '../lib/dispatcher.js'
import { calculateEta } from '../lib/time.js'
const Checkbox = require('material-ui/Checkbox').default
const LinearProgress = require('material-ui/LinearProgress').default
const Checkbox = checkbox.default
const LinearProgress = linearProgress.default
const TorrentSummary = require('../lib/torrent-summary')
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 () {
const state = this.props.state
@@ -38,11 +35,7 @@ export default class TorrentList extends React.Component {
)
return (
<div
key='torrent-list'
className='torrent-list'
onContextMenu={dispatcher('openTorrentListContextMenu')}
>
<div key='torrent-list' className='torrent-list'>
{contents}
</div>
)
@@ -198,9 +191,18 @@ export default class TorrentList extends React.Component {
const downloadSpeed = prog.downloadSpeed
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 () {
@@ -282,17 +284,10 @@ export default class TorrentList extends React.Component {
)
} else {
// We do know the files. List them and show download stats for each one
const sortByName = this.props.state.saved.prefs.sortByName
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
let fileRows = torrentSummary.files
const fileRows = torrentSummary.files
.filter((file) => !file.path.includes('/.____padding_file/'))
.map((file, index) => ({ file, 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))
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
filesElement = (
<div key='files' className='files'>
@@ -344,7 +339,7 @@ export default class TorrentList extends React.Component {
} else {
icon = 'description' /* file icon, opens in OS default app */
handleClick = isDone
? dispatcher('openPath', infoHash, index)
? dispatcher('openItem', infoHash, index)
: (e) => e.stopPropagation() // noop if file is not ready
}
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed

View File

@@ -1,33 +1,38 @@
/* eslint-disable import/first */
// To keep the UI snappy, we run WebTorrent in its own hidden window, a separate
// process from the main window.
console.time('init')
import crypto from 'crypto'
import util from 'util'
import { announceList as defaultAnnounceList } from 'create-torrent'
import electron from 'electron'
import fs from 'fs'
import * as mm from 'music-metadata'
import networkAddress from 'network-address'
import path from 'path'
import WebTorrent from 'webtorrent'
const crypto = require('crypto')
const deepEqual = require('deep-equal')
const defaultAnnounceList = require('create-torrent').announceList
const electron = require('electron')
const fs = require('fs')
const mkdirp = require('mkdirp')
const mm = require('music-metadata')
const networkAddress = require('network-address')
const path = require('path')
const WebTorrent = require('webtorrent')
import config from '../config.js'
import { TorrentKeyNotFoundError } from './lib/errors.js'
import torrentPoster from './lib/torrent-poster.js'
const crashReporter = require('../crash-reporter')
const config = require('../config')
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
globalThis.WEBTORRENT_ANNOUNCE = defaultAnnounceList
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
.map((arr) => arr[0])
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
/**
* 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.
@@ -68,29 +73,29 @@ init()
function init () {
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))
ipcRenderer.on('wt-stop-torrenting', (e, infoHash) =>
ipc.on('wt-stop-torrenting', (e, infoHash) =>
stopTorrenting(infoHash))
ipcRenderer.on('wt-create-torrent', (e, torrentKey, options) =>
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
createTorrent(torrentKey, options))
ipcRenderer.on('wt-save-torrent-file', (e, torrentKey) =>
ipc.on('wt-save-torrent-file', (e, torrentKey) =>
saveTorrentFile(torrentKey))
ipcRenderer.on('wt-generate-torrent-poster', (e, torrentKey) =>
ipc.on('wt-generate-torrent-poster', (e, torrentKey) =>
generateTorrentPoster(torrentKey))
ipcRenderer.on('wt-get-audio-metadata', (e, infoHash, index) =>
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
getAudioMetadata(infoHash, index))
ipcRenderer.on('wt-start-server', (e, infoHash) =>
ipc.on('wt-start-server', (e, infoHash) =>
startServer(infoHash))
ipcRenderer.on('wt-stop-server', () =>
ipc.on('wt-stop-server', (e) =>
stopServer())
ipcRenderer.on('wt-select-files', (e, infoHash, selections) =>
ipc.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections))
ipcRenderer.send('ipcReadyWebTorrent')
ipc.send('ipcReadyWebTorrent')
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)
setInterval(updateTorrentProgress, 1000)
@@ -98,8 +103,8 @@ function init () {
}
function listenToClientEvents () {
client.on('warning', (err) => ipcRenderer.send('wt-warning', null, err.message))
client.on('error', (err) => ipcRenderer.send('wt-error', null, err.message))
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message))
}
// 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)
const torrent = client.add(torrentID, {
path,
fileModtimes
path: path,
fileModtimes: fileModtimes
})
torrent.key = torrentKey
@@ -133,44 +138,44 @@ function createTorrent (torrentKey, options) {
const torrent = client.seed(paths, options)
torrent.key = torrentKey
addTorrentEvents(torrent)
ipcRenderer.send('wt-new-torrent')
ipc.send('wt-new-torrent')
}
function addTorrentEvents (torrent) {
torrent.on('warning', (err) =>
ipcRenderer.send('wt-warning', torrent.key, err.message))
ipc.send('wt-warning', torrent.key, err.message))
torrent.on('error', (err) =>
ipcRenderer.send('wt-error', torrent.key, err.message))
ipc.send('wt-error', torrent.key, err.message))
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('ready', torrentReady)
torrent.on('done', torrentDone)
function torrentMetadata () {
const info = getTorrentInfo(torrent)
ipcRenderer.send('wt-metadata', torrent.key, info)
ipc.send('wt-metadata', torrent.key, info)
updateTorrentProgress()
}
function torrentReady () {
const info = getTorrentInfo(torrent)
ipcRenderer.send('wt-ready', torrent.key, info)
ipcRenderer.send('wt-ready-' + torrent.infoHash, torrent.key, info)
ipc.send('wt-ready', torrent.key, info)
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
updateTorrentProgress()
}
function torrentDone () {
const info = getTorrentInfo(torrent)
ipcRenderer.send('wt-done', torrent.key, info)
ipc.send('wt-done', torrent.key, info)
updateTorrentProgress()
torrent.getFileModtimes((err, fileModtimes) => {
torrent.getFileModtimes(function (err, fileModtimes) {
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 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'
if (!err) {
// 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
fs.mkdir(config.TORRENT_PATH, { recursive: true }, _ => {
fs.writeFile(torrentPath, torrent.torrentFile, err => {
mkdirp(config.TORRENT_PATH, function (_) {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath)
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
function generateTorrentPoster (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)
// 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)
const posterFileName = torrent.infoHash + extension
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)
// show the poster
ipcRenderer.send('wt-poster', torrentKey, posterFileName)
ipc.send('wt-poster', torrentKey, posterFileName)
})
})
})
@@ -244,23 +249,25 @@ function generateTorrentPoster (torrentKey) {
function updateTorrentProgress () {
const progress = getTorrentProgress()
// 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 */
}
ipcRenderer.send('wt-progress', progress)
ipc.send('wt-progress', progress)
prevProgress = progress
}
function getTorrentProgress () {
// First, track overall 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
// TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield
const torrentProg = client.torrents.map(torrent => {
const fileProg = torrent.files && torrent.files.map(file => {
const torrentProg = client.torrents.map(function (torrent) {
const fileProg = torrent.files && torrent.files.map(function (file, index) {
const numPieces = file._endPiece - file._startPiece + 1
let numPiecesPresent = 0
for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
@@ -300,12 +307,12 @@ function startServer (infoHash) {
else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
}
function startServerFromReadyTorrent (torrent) {
function startServerFromReadyTorrent (torrent, cb) {
if (server) return
// start the streaming torrent-to-http server
server = torrent.createServer()
server.listen(0, () => {
server.listen(0, function () {
const port = server.address().port
const urlSuffix = ':' + port
const info = {
@@ -315,8 +322,8 @@ function startServerFromReadyTorrent (torrent) {
networkAddress: networkAddress()
}
ipcRenderer.send('wt-server-running', info)
ipcRenderer.send('wt-server-' + torrent.infoHash, info)
ipc.send('wt-server-running', info)
ipc.send('wt-server-' + torrent.infoHash, info)
})
}
@@ -334,31 +341,25 @@ function getAudioMetadata (infoHash, index) {
// Set initial matadata to display the filename first.
const metadata = { title: file.name }
ipcRenderer.send('wt-audio-metadata', infoHash, index, metadata)
ipc.send('wt-audio-metadata', infoHash, index, metadata)
const options = {
native: false,
skipCovers: true,
fileSize: file.length,
observer: () => {
ipcRenderer.send('wt-audio-metadata', infoHash, index, {
common: metadata.common,
format: metadata.format
})
observer: event => {
ipc.send('wt-audio-metadata', infoHash, index, event.metadata)
}
}
const onMetadata = file.done
const onMetaData = file.done
// If completed; use direct file access
? mm.parseFile(path.join(torrent.path, file.path), options)
// otherwise stream
: mm.parseStream(file.createReadStream(), file.name, options)
onMetadata
onMetaData
.then(
metadata => {
ipcRenderer.send('wt-audio-metadata', infoHash, index, metadata)
console.log(`metadata for file='${file.name}' completed.`)
},
() => console.log(`metadata for file='${file.name}' completed.`),
err => {
console.log(
`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
// select/deselect files later on
if (!selections) {
selections = new Array(torrent.files.length).fill(true)
selections = torrent.files.map((x) => true)
}
// Selections specified incorrectly?
@@ -398,15 +399,15 @@ function selectFiles (torrentOrInfoHash, selections) {
torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files)
selections.forEach((selection, i) => {
for (let i = 0; i < selections.length; i++) {
const file = torrent.files[i]
if (selection) {
if (selections[i]) {
file.select()
} else {
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
file.deselect()
}
})
}
}
// Gets a WebTorrent handle by torrentKey
@@ -424,7 +425,7 @@ function onError (err) {
// TODO: remove this once the following bugs are fixed:
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
// https://github.com/electron/electron/issues/7212
window.testOfflineMode = () => {
window.testOfflineMode = function () {
console.log('Test, going OFFLINE')
client = window.client = new WebTorrent({
peerId: PEER_ID,

View File

@@ -32,8 +32,8 @@
<p>
Version <script>document.write(require('../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><script>document.write(require('../build/config').APP_COPYRIGHT)</script></p>
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
</body>
</html>

Binary file not shown.

Binary file not shown.

View File

@@ -1,25 +1,26 @@
[Desktop Entry]
Type=Application
<% if (version) { %>Version=<%= version %><% } %>
Name=<%= productName %>
<% if (genericName) { %>GenericName=<%= genericName %><% } %>
<% if (description) { %>Comment=<%= description %><% } %>
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
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
<% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;<% } %>
<% if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;<% } %>
StartupNotify=true
StartupWMClass=<%= productName %>
<% if (name) { %>StartupWMClass=<%= name %> <% } %>
[Desktop Action CreateNewTorrent]
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]
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]
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

View File

@@ -322,10 +322,6 @@ textarea,
* TORRENT LIST
*/
.torrent-list {
height: 100%;
}
.torrent {
background: linear-gradient(to bottom right, #4B79A1, #283E51);
background-repeat: no-repeat;
@@ -562,13 +558,6 @@ body.drag .app::after {
width: 100%;
}
.player .name {
font-size: 20px;
font-weight: bold;
line-height: 1.5em;
margin-bottom: 6px;
}
/*
* PLAYER CONTROLS
*/
@@ -634,11 +623,6 @@ body.drag .app::after {
margin-top: 6px;
}
.player .controls .icon.multi-audio {
font-size: 26px;
margin-top: 6px;
}
.player .controls .icon.fullscreen {
font-size: 26px;
margin-right: 15px;

View File

@@ -8,6 +8,6 @@
</head>
<body>
<div id='body'></div>
<script type="module" src="../build/renderer/main.js"></script>
<script>require('../build/renderer/main.js')</script>
</body>
</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